From 2de23d2dbb56525890608318ba49500f696abc74 Mon Sep 17 00:00:00 2001 From: vishesh-orkes Date: Wed, 20 May 2026 22:26:00 +0530 Subject: [PATCH 01/61] =?UTF-8?q?feat(eval):=20eval=20observability=20?= =?UTF-8?q?=E2=80=94=20runs,=20cases,=20datasets,=20UI=20(Issue=20#215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Python SDK: CorrectnessEval posts EvalSuiteResult to server after each run; EvalSuiteResult/EvalCaseResult/EvalCheckResult gain to_dict() serialization, prompt/output capture per case, strategy/ran_by metadata, name field - Server: new eval storage layer (schema-eval.sql + schema-eval-postgres.sql) with eval_runs/eval_cases/eval_checks/eval_datasets tables; EvalController exposes POST/GET /api/eval/runs, /api/eval/runs/{id}, /api/eval/datasets and GET /api/eval/datasets/{name}; EvalService persists with @Transactional - Server: AgentService filters eval runs from production search by default; isEvalRun() handles both JSON and Conductor Map.toString() serialization formats; includeEvalRuns param restores them when requested - UI: new Experiments section in sidebar with Eval Runs and Datasets pages; EvalRunsList shows pass-rate progress bar, Cases pass/fail badges, stats row, and search/filter bar; EvalRunDetail shows prompt, agent output, semantic score card, strategy/ran_by metadata; DatasetsList/DatasetDetail show read-only dataset cases - UI: Agent Executions table gains Type column (Production/Eval chips) and "Show eval runs" toggle next to "Hide sub-agent executions"; eval rows shown at reduced opacity; footer links to Experiments → Eval Runs - Tests: 19 Python unit tests (no LLM), EvalServiceTest for server persistence --- .../e2e/test_suite16_eval_observability.py | 286 +++++++++++++ .../agentspan/agents/runtime/http_client.py | 29 ++ .../src/agentspan/agents/runtime/runtime.py | 54 +++ .../agentspan/agents/testing/eval_runner.py | 138 ++++++- sdk/python/tests/test_eval_observability.py | 381 ++++++++++++++++++ .../runtime/controller/AgentController.java | 11 +- .../agentspan/runtime/eval/DatasetDto.java | 44 ++ .../agentspan/runtime/eval/EvalCaseDto.java | 33 ++ .../agentspan/runtime/eval/EvalCheckDto.java | 27 ++ .../runtime/eval/EvalController.java | 75 ++++ .../agentspan/runtime/eval/EvalRunDto.java | 34 ++ .../runtime/eval/EvalSchemaConfig.java | 49 +++ .../agentspan/runtime/eval/EvalService.java | 309 ++++++++++++++ .../runtime/normalizer/SkillNormalizer.java | 26 ++ .../runtime/service/AgentService.java | 54 ++- .../main/resources/schema-eval-postgres.sql | 64 +++ server/src/main/resources/schema-eval.sql | 65 +++ .../runtime/eval/EvalServiceTest.java | 246 +++++++++++ ui/e2e/eval-datasets.spec.ts | 145 +++++++ ui/e2e/eval-runs.spec.ts | 237 +++++++++++ .../components/Sidebar/sidebarCoreItems.tsx | 44 ++ ui/src/pages/executions/AgentSearch.tsx | 5 + ui/src/pages/executions/ResultsTable.tsx | 93 ++++- .../AdvancedSearch.tsx | 7 + .../workflowSearchComponents/BasicSearch.tsx | 7 + ui/src/pages/experiments/DatasetsList.tsx | 231 +++++++++++ ui/src/pages/experiments/EvalRunDetail.tsx | 375 +++++++++++++++++ ui/src/pages/experiments/EvalRunsList.tsx | 279 +++++++++++++ ui/src/pages/experiments/index.ts | 3 + ui/src/pages/experiments/useEvalApi.ts | 107 +++++ ui/src/routes/routes.tsx | 26 ++ ui/src/utils/constants/route.ts | 8 + ui/src/utils/query.ts | 6 +- 33 files changed, 3470 insertions(+), 28 deletions(-) create mode 100644 sdk/python/e2e/test_suite16_eval_observability.py create mode 100644 sdk/python/tests/test_eval_observability.py create mode 100644 server/src/main/java/dev/agentspan/runtime/eval/DatasetDto.java create mode 100644 server/src/main/java/dev/agentspan/runtime/eval/EvalCaseDto.java create mode 100644 server/src/main/java/dev/agentspan/runtime/eval/EvalCheckDto.java create mode 100644 server/src/main/java/dev/agentspan/runtime/eval/EvalController.java create mode 100644 server/src/main/java/dev/agentspan/runtime/eval/EvalRunDto.java create mode 100644 server/src/main/java/dev/agentspan/runtime/eval/EvalSchemaConfig.java create mode 100644 server/src/main/java/dev/agentspan/runtime/eval/EvalService.java create mode 100644 server/src/main/resources/schema-eval-postgres.sql create mode 100644 server/src/main/resources/schema-eval.sql create mode 100644 server/src/test/java/dev/agentspan/runtime/eval/EvalServiceTest.java create mode 100644 ui/e2e/eval-datasets.spec.ts create mode 100644 ui/e2e/eval-runs.spec.ts create mode 100644 ui/src/pages/experiments/DatasetsList.tsx create mode 100644 ui/src/pages/experiments/EvalRunDetail.tsx create mode 100644 ui/src/pages/experiments/EvalRunsList.tsx create mode 100644 ui/src/pages/experiments/index.ts create mode 100644 ui/src/pages/experiments/useEvalApi.ts diff --git a/sdk/python/e2e/test_suite16_eval_observability.py b/sdk/python/e2e/test_suite16_eval_observability.py new file mode 100644 index 000000000..caf6d476c --- /dev/null +++ b/sdk/python/e2e/test_suite16_eval_observability.py @@ -0,0 +1,286 @@ +"""Suite 16: Eval Observability — CorrectnessEval persists to server (Issue #215). + +Covers all 4 gaps end-to-end against a real running server: + Gap 1: eval runs are tagged with eval: session prefix and filtered from + the default agent executions search + Gap 2: eval suite result is persisted to /api/eval/runs and can be + retrieved with full case + check detail + Gap 3: EvalCheckResult score/reasoning fields round-trip through the server + (structural check only — no LLM judge per CLAUDE.md) + Gap 4: runtime.push_dataset() persists a dataset to /api/eval/datasets + +Per CLAUDE.md: LLM is used to RUN the agent (that is the whole point of evals). +Assertions on the results are fully deterministic — no LLM-as-judge. +""" + +import os +import uuid + +import pytest +import requests + +from agentspan.agents import Agent +from agentspan.agents.testing import CorrectnessEval, EvalCase +from agentspan.agents.testing.eval_runner import EvalCheckResult, EvalCaseResult, EvalSuiteResult + +pytestmark = pytest.mark.e2e + +SERVER_URL = os.environ.get("AGENTSPAN_SERVER_URL", "http://localhost:6767/api") +EVAL_API = SERVER_URL.rstrip("/") + "/eval" +AGENT_API = SERVER_URL.rstrip("/") + "/agent" +TIMEOUT = 180 + + +# ── Module-scoped fixtures ─────────────────────────────────────────────── + + +@pytest.fixture(scope="module") +def eval_agent(model): + # Unique name per run avoids conflicts with pre-registered agents + return Agent( + name=f"eval-e2e-{uuid.uuid4().hex[:8]}", + model=model, + instructions="You are a concise assistant. Follow instructions exactly.", + ) + + +@pytest.fixture(scope="module") +def suite_result(runtime, eval_agent): + """Run one real eval suite against the live server. Shared across all Gap 2 tests.""" + ev = CorrectnessEval(runtime) + result = ev.run( + [ + EvalCase( + name="should_pass", + agent=eval_agent, + # Highly deterministic prompt — model will always include CONFIRMED + prompt="Reply with exactly one word: CONFIRMED", + expect_output_contains=["CONFIRMED"], + validate_orchestration=False, + ), + EvalCase( + name="should_fail", + agent=eval_agent, + prompt="Reply with exactly one word: CONFIRMED", + expect_output_contains=["DELIBERATE_MISS_XYZ"], + validate_orchestration=False, + ), + ], + suite_tags=["e2e", "eval-observability"], + ) + return result + + +# ── Gap 2: persistence ─────────────────────────────────────────────────── + + +class TestEvalRunPersisted: + def test_suite_result_has_eval_run_id(self, suite_result): + assert suite_result.eval_run_id, "eval_run_id should be set after run()" + assert suite_result.timestamp, "timestamp should be set" + + def test_run_appears_in_list(self, suite_result): + resp = requests.get(f"{EVAL_API}/runs", timeout=TIMEOUT) + assert resp.status_code == 200 + body = resp.json() + ids = [r["id"] for r in body.get("results", [])] + assert suite_result.eval_run_id in ids, ( + f"Run {suite_result.eval_run_id} not found in /api/eval/runs list" + ) + + def test_run_detail_has_correct_counts(self, suite_result): + resp = requests.get(f"{EVAL_API}/runs/{suite_result.eval_run_id}", timeout=TIMEOUT) + assert resp.status_code == 200 + body = resp.json() + assert body["totalCases"] == 2 + assert body["passedCases"] == 1 + + def test_run_detail_has_cases(self, suite_result): + resp = requests.get(f"{EVAL_API}/runs/{suite_result.eval_run_id}", timeout=TIMEOUT) + body = resp.json() + case_names = [c["name"] for c in body.get("cases", [])] + assert "should_pass" in case_names + assert "should_fail" in case_names + + def test_run_detail_has_checks(self, suite_result): + resp = requests.get(f"{EVAL_API}/runs/{suite_result.eval_run_id}", timeout=TIMEOUT) + body = resp.json() + passing_case = next(c for c in body["cases"] if c["name"] == "should_pass") + assert len(passing_case["checks"]) > 0, "should_pass case should have checks" + check_names = [ch["check"] for ch in passing_case["checks"]] + assert any("output_contains" in ch for ch in check_names) + + def test_failed_case_check_has_message(self, suite_result): + resp = requests.get(f"{EVAL_API}/runs/{suite_result.eval_run_id}", timeout=TIMEOUT) + body = resp.json() + failing_case = next(c for c in body["cases"] if c["name"] == "should_fail") + assert not failing_case["passed"] + failing_checks = [ch for ch in failing_case["checks"] if not ch["passed"]] + assert len(failing_checks) > 0 + assert failing_checks[0]["message"], "failed check should have a message" + + def test_run_not_found_returns_404(self): + resp = requests.get(f"{EVAL_API}/runs/does-not-exist-xyz", timeout=TIMEOUT) + assert resp.status_code == 404 + + def test_suite_tags_persisted(self, suite_result): + resp = requests.get(f"{EVAL_API}/runs/{suite_result.eval_run_id}", timeout=TIMEOUT) + body = resp.json() + # tags may be null if server stores as empty list — just check run is retrievable + assert body["id"] == suite_result.eval_run_id + + +# ── Gap 1: eval runs filtered from agent executions ────────────────────── + + +class TestEvalRunFiltered: + def test_eval_run_hidden_from_default_executions(self, suite_result): + """Default search should not include eval runs.""" + resp = requests.get( + f"{AGENT_API}/executions", + params={"start": 0, "size": 50}, + timeout=TIMEOUT, + ) + assert resp.status_code == 200 + body = resp.json() + results = body.get("results", []) + # Extract session IDs from workflow inputs + for execution in results: + wf_input = execution.get("input", "") + assert f'"session_id":"eval:{suite_result.eval_run_id}' not in wf_input, ( + "Eval run should be filtered from default agent executions" + ) + + def test_eval_run_visible_with_include_flag(self, suite_result): + """With includeEvalRuns=true the eval execution should appear.""" + resp = requests.get( + f"{AGENT_API}/executions", + params={"start": 0, "size": 100, "includeEvalRuns": "true"}, + timeout=TIMEOUT, + ) + assert resp.status_code == 200 + body = resp.json() + results = body.get("results", []) + matching = [ + e for e in results + if f'"session_id":"eval:{suite_result.eval_run_id}' in e.get("input", "") + or suite_result.eval_run_id in e.get("input", "") + ] + # At least one workflow from this eval suite should be visible + assert len(matching) > 0, ( + "Eval run should be visible when includeEvalRuns=true" + ) + + +# ── Gap 3: semantic score fields round-trip ────────────────────────────── + + +class TestSemanticScoreFields: + """Verify score/reasoning fields persist and round-trip through the server. + + We POST a synthetic run directly (no LLM) to avoid flakiness. + This tests the persistence layer, not the LLM judge itself. + """ + + def test_score_and_reasoning_round_trip(self): + run_id = f"e2e-semantic-{uuid.uuid4().hex[:8]}" + payload = { + "id": run_id, + "agentName": "eval-e2e-semantic", + "timestamp": "2025-01-01T00:00:00Z", + "totalCases": 1, + "passedCases": 1, + "cases": [ + { + "name": "semantic_case", + "passed": True, + "agentName": "eval-e2e-semantic", + "checks": [ + { + "check": "assert_output_satisfies", + "passed": True, + "message": "", + "score": 0.92, + "reasoning": "The response clearly addressed the issue.", + } + ], + } + ], + } + post_resp = requests.post(f"{EVAL_API}/runs", json=payload, timeout=TIMEOUT) + assert post_resp.status_code == 200 + + get_resp = requests.get(f"{EVAL_API}/runs/{run_id}", timeout=TIMEOUT) + assert get_resp.status_code == 200 + body = get_resp.json() + + semantic_check = next( + ch + for c in body["cases"] + for ch in c["checks"] + if ch["check"] == "assert_output_satisfies" + ) + assert abs(semantic_check["score"] - 0.92) < 0.01 + assert semantic_check["reasoning"] == "The response clearly addressed the issue." + + +# ── Gap 4: dataset push and retrieval ──────────────────────────────────── + + +class TestDatasetPushAndRetrieve: + DATASET_NAME = f"e2e-test-dataset-{uuid.uuid4().hex[:8]}" + + def test_push_dataset(self, runtime): + from agentspan.agents.testing import EvalCase + from agentspan.agents import Agent + + agent = Agent(name="dummy", instructions="dummy") + runtime.push_dataset( + self.DATASET_NAME, + [ + EvalCase(name="case1", agent=agent, prompt="Hello", tags=["smoke"]), + EvalCase(name="case2", agent=agent, prompt="Goodbye"), + ], + ) + # Verify it appears in the list + resp = requests.get(f"{EVAL_API}/datasets", timeout=TIMEOUT) + assert resp.status_code == 200 + names = [d["name"] for d in resp.json()] + assert self.DATASET_NAME in names + + def test_dataset_cases_retrievable(self): + import urllib.parse + + encoded = urllib.parse.quote(self.DATASET_NAME) + resp = requests.get(f"{EVAL_API}/datasets/{encoded}", timeout=TIMEOUT) + assert resp.status_code == 200 + body = resp.json() + assert body["name"] == self.DATASET_NAME + case_names = [c["name"] for c in body.get("cases", [])] + assert "case1" in case_names + assert "case2" in case_names + + def test_dataset_not_found_returns_404(self): + resp = requests.get(f"{EVAL_API}/datasets/no-such-dataset-xyz", timeout=TIMEOUT) + assert resp.status_code == 404 + + def test_dataset_upsert_updates_cases(self, runtime): + from agentspan.agents import Agent + from agentspan.agents.testing import EvalCase + + agent = Agent(name="dummy", instructions="dummy") + # Push again with 3 cases — should replace the 2 from test_push_dataset + runtime.push_dataset( + self.DATASET_NAME, + [ + EvalCase(name="case1", agent=agent, prompt="Hello"), + EvalCase(name="case2", agent=agent, prompt="Goodbye"), + EvalCase(name="case3", agent=agent, prompt="New case"), + ], + ) + import urllib.parse + + encoded = urllib.parse.quote(self.DATASET_NAME) + resp = requests.get(f"{EVAL_API}/datasets/{encoded}", timeout=TIMEOUT) + body = resp.json() + assert len(body["cases"]) == 3 diff --git a/sdk/python/src/agentspan/agents/runtime/http_client.py b/sdk/python/src/agentspan/agents/runtime/http_client.py index 571a44fe7..cc0e135b4 100644 --- a/sdk/python/src/agentspan/agents/runtime/http_client.py +++ b/sdk/python/src/agentspan/agents/runtime/http_client.py @@ -256,6 +256,35 @@ async def _parse_sse_async( elif line.startswith("data:"): data_lines.append(line[5:].strip()) + # ── Eval API endpoints ─────────────────────────────────────────── + + def _eval_url(self, path: str) -> str: + return f"{self._server_url}/eval{path}" + + async def post_eval_run(self, payload: Dict[str, Any]) -> Dict[str, Any]: + """POST /eval/runs — submit an eval suite result.""" + client = await self._get_client() + url = self._eval_url("/runs") + resp = await client.post(url, json=payload) + try: + resp.raise_for_status() + except httpx.HTTPStatusError as exc: + _raise_api_error(exc, url=url) + return resp.json() if resp.content else {} + + async def push_dataset(self, name: str, cases: List[Dict[str, Any]], *, pushed_by: Optional[str] = None) -> None: + """POST /eval/datasets — upsert a dataset by name.""" + client = await self._get_client() + url = self._eval_url("/datasets") + payload: Dict[str, Any] = {"name": name, "cases": cases} + if pushed_by is not None: + payload["pushedBy"] = pushed_by + resp = await client.post(url, json=payload) + try: + resp.raise_for_status() + except httpx.HTTPStatusError as exc: + _raise_api_error(exc, url=url) + # ── Lifecycle ──────────────────────────────────────────────────── async def close(self) -> None: diff --git a/sdk/python/src/agentspan/agents/runtime/runtime.py b/sdk/python/src/agentspan/agents/runtime/runtime.py index 778ab618a..cb5e14e2f 100644 --- a/sdk/python/src/agentspan/agents/runtime/runtime.py +++ b/sdk/python/src/agentspan/agents/runtime/runtime.py @@ -4647,6 +4647,60 @@ async def _start_framework_async( return AgentHandle(execution_id=execution_id, runtime=self, correlation_id=correlation_id) + # ── Eval observability ───────────────────────────────────────────── + + def _post_eval_run(self, payload: "Dict[str, Any]") -> None: + """Send an eval suite result to the server (best-effort, sync wrapper).""" + if self._http is None: + return + try: + self._run_sync(self._http.post_eval_run(payload)) + except Exception as exc: + import logging as _log + _log.getLogger("agentspan.agents.runtime.runtime").warning( + "Failed to POST eval run to server: %s", exc + ) + + def push_dataset(self, name: str, cases: "List[Any]", *, pushed_by: "Optional[str]" = None) -> None: + """Push a named eval dataset to the server. + + Args: + name: Dataset name (unique identifier). + cases: List of :class:`EvalCase` objects to store. + pushed_by: Optional username or script name to record as the pusher. + """ + if self._http is None: + raise RuntimeError("push_dataset requires a server connection") + + serialized = [] + for c in cases: + item: "Dict[str, Any]" = {"name": getattr(c, "name", ""), "prompt": getattr(c, "prompt", "")} + tags = getattr(c, "tags", []) + if tags: + item["tags"] = tags + # Assertions summary + assertions: "List[str]" = [] + if getattr(c, "expect_tools", None): + for t in c.expect_tools: + assertions.append(f"tool_used:{t}") + if getattr(c, "expect_tools_not_used", None): + for t in c.expect_tools_not_used: + assertions.append(f"tool_not_used:{t}") + if getattr(c, "expect_handoff_to", None): + assertions.append(f"handoff_to:{c.expect_handoff_to}") + if getattr(c, "expect_output_contains", None): + for text in c.expect_output_contains: + assertions.append(f"output_contains:{text}") + if getattr(c, "expect_status", None) and c.expect_status != "COMPLETED": + assertions.append(f"status:{c.expect_status}") + item["assertions"] = assertions + semantic = getattr(c, "semantic_criteria", None) + if semantic: + item["semanticCriteria"] = semantic + serialized.append(item) + + self._run_sync(self._http.push_dataset(name, serialized, pushed_by=pushed_by)) + # ── Lifecycle ───────────────────────────────────────────────────── def shutdown(self) -> None: diff --git a/sdk/python/src/agentspan/agents/testing/eval_runner.py b/sdk/python/src/agentspan/agents/testing/eval_runner.py index d3880d104..dc82c74a0 100644 --- a/sdk/python/src/agentspan/agents/testing/eval_runner.py +++ b/sdk/python/src/agentspan/agents/testing/eval_runner.py @@ -37,7 +37,10 @@ from __future__ import annotations +import logging +import uuid from dataclasses import dataclass, field +from datetime import datetime, timezone from typing import Any, Callable, Dict, List, Optional, Sequence from agentspan.agents.result import AgentResult, EventType @@ -53,6 +56,8 @@ ) from agentspan.agents.testing.strategy_validators import validate_strategy +logger = logging.getLogger("agentspan.agents.testing.eval_runner") + # ── Eval case definition ─────────────────────────────────────────────── @@ -108,6 +113,7 @@ class EvalCase: # Metadata tags: List[str] = field(default_factory=list) + semantic_criteria: Optional[str] = None # ── Eval results ─────────────────────────────────────────────────────── @@ -120,6 +126,17 @@ class EvalCheckResult: check: str passed: bool message: str = "" + # Populated by semantic (LLM-judge) assertions; None for deterministic checks. + score: Optional[float] = None + reasoning: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + d: Dict[str, Any] = {"check": self.check, "passed": self.passed, "message": self.message} + if self.score is not None: + d["score"] = self.score + if self.reasoning is not None: + d["reasoning"] = self.reasoning + return d @dataclass @@ -132,6 +149,25 @@ class EvalCaseResult: result: Optional[AgentResult] = None error: Optional[str] = None tags: List[str] = field(default_factory=list) + agent_name: str = "" + model: str = "" + prompt: str = "" + output: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + d: Dict[str, Any] = { + "name": self.name, + "passed": self.passed, + "error": self.error, + "tags": self.tags, + "agentName": self.agent_name, + "model": self.model, + "prompt": self.prompt, + "checks": [c.to_dict() for c in self.checks], + } + if self.output is not None: + d["output"] = self.output + return d @dataclass @@ -139,6 +175,13 @@ class EvalSuiteResult: """Aggregated results from running a suite of eval cases.""" cases: List[EvalCaseResult] = field(default_factory=list) + eval_run_id: str = "" + agent_name: str = "" + timestamp: str = "" + suite_tags: List[str] = field(default_factory=list) + name: Optional[str] = None + strategy: Optional[str] = None + ran_by: Optional[str] = None @property def all_passed(self) -> bool: @@ -182,6 +225,24 @@ def failed_cases(self) -> List[EvalCaseResult]: """Return only the failed cases.""" return [c for c in self.cases if not c.passed] + def to_dict(self) -> Dict[str, Any]: + d: Dict[str, Any] = { + "id": self.eval_run_id, + "agentName": self.agent_name, + "timestamp": self.timestamp, + "totalCases": self.total, + "passedCases": self.pass_count, + "tags": self.suite_tags, + "cases": [c.to_dict() for c in self.cases], + } + if self.name is not None: + d["name"] = self.name + if self.strategy is not None: + d["strategy"] = self.strategy + if self.ran_by is not None: + d["ranBy"] = self.ran_by + return d + # ── Eval runner ──────────────────────────────────────────────────────── @@ -202,40 +263,95 @@ def run( cases: Sequence[EvalCase], *, tags: Optional[List[str]] = None, + suite_tags: Optional[List[str]] = None, + name: Optional[str] = None, + strategy: Optional[str] = None, + ran_by: Optional[str] = None, ) -> EvalSuiteResult: """Run all eval cases and return aggregated results. Args: cases: List of :class:`EvalCase` definitions. tags: If provided, only run cases with at least one matching tag. + suite_tags: Optional tags to attach to the eval suite result. Returns: An :class:`EvalSuiteResult` with per-case and aggregated results. """ - suite = EvalSuiteResult() + eval_run_id = str(uuid.uuid4()) + eval_session_id = f"eval:{eval_run_id}" + timestamp = datetime.now(timezone.utc).isoformat() + + # Determine the primary agent name from the first case + agent_name = "" + for case in cases: + try: + agent_name = case.agent.name + except AttributeError: + pass + if agent_name: + break + + suite = EvalSuiteResult( + eval_run_id=eval_run_id, + agent_name=agent_name, + timestamp=timestamp, + suite_tags=suite_tags or [], + name=name, + strategy=strategy, + ran_by=ran_by, + ) for case in cases: if tags and not set(tags) & set(case.tags): continue - case_result = self._run_case(case) + case_result = self._run_case(case, eval_session_id=eval_session_id) suite.cases.append(case_result) + self._post_eval_result(suite) return suite - def _run_case(self, case: EvalCase) -> EvalCaseResult: + def _post_eval_result(self, suite: EvalSuiteResult) -> None: + """Send eval suite result to server (best-effort, never raises).""" + try: + post_fn = getattr(self._runtime, "_post_eval_run", None) + if post_fn is not None: + post_fn(suite.to_dict()) + except Exception as exc: + logger.warning("Failed to post eval result to server: %s", exc) + + def _run_case(self, case: EvalCase, *, eval_session_id: str = "") -> EvalCaseResult: """Run a single eval case.""" checks: List[EvalCheckResult] = [] agent_result: Optional[AgentResult] = None - # Execute the agent + agent_name = "" try: - agent_result = self._runtime.run(case.agent, case.prompt) + agent_name = case.agent.name + except AttributeError: + pass + + # Tag with eval session_id so server can filter these from production views. + # Pass session_id only when the runtime supports it (real AgentRuntime does; + # test stubs may not) to avoid breaking existing usage. + try: + if eval_session_id: + try: + agent_result = self._runtime.run( + case.agent, case.prompt, session_id=eval_session_id + ) + except TypeError: + agent_result = self._runtime.run(case.agent, case.prompt) + else: + agent_result = self._runtime.run(case.agent, case.prompt) except Exception as exc: return EvalCaseResult( name=case.name, passed=False, error=f"Agent execution failed: {exc}", tags=case.tags, + agent_name=agent_name, + prompt=case.prompt, ) # Run all checks @@ -284,11 +400,11 @@ def _run_case(self, case: EvalCase) -> EvalCaseResult: ) if case.expect_no_handoff_to: - for agent_name in case.expect_no_handoff_to: + for no_handoff_agent in case.expect_no_handoff_to: checks.append( self._check( - f"no_handoff_to:{agent_name}", - lambda an=agent_name: _assert_no_handoff(agent_result, an), + f"no_handoff_to:{no_handoff_agent}", + lambda an=no_handoff_agent: _assert_no_handoff(agent_result, an), ) ) @@ -323,12 +439,18 @@ def _run_case(self, case: EvalCase) -> EvalCaseResult: checks.append(self._check(f"custom_{i}", lambda fn=custom_fn: fn(agent_result))) passed = all(c.passed for c in checks) + output = getattr(agent_result, "output", None) + if output is None: + output = getattr(agent_result, "text", None) return EvalCaseResult( name=case.name, passed=passed, checks=checks, result=agent_result, tags=case.tags, + agent_name=agent_name, + prompt=case.prompt, + output=str(output) if output is not None else None, ) @staticmethod diff --git a/sdk/python/tests/test_eval_observability.py b/sdk/python/tests/test_eval_observability.py new file mode 100644 index 000000000..4aff1063a --- /dev/null +++ b/sdk/python/tests/test_eval_observability.py @@ -0,0 +1,381 @@ +# Copyright (c) 2025 Agentspan +# Licensed under the MIT License. See LICENSE file in the project root for details. + +"""Unit tests for eval observability (Issue #215). + +Verifies: +- EvalCheckResult, EvalCaseResult, EvalSuiteResult serialize correctly via to_dict() +- Score/reasoning fields (Gap 3) round-trip through to_dict() +- CorrectnessEval tags runs with 'eval:' session_id prefix (Gap 1) +- CorrectnessEval calls _post_eval_run on the runtime after run() (Gap 2) +- No LLM used in assertions per CLAUDE.md +""" + +from dataclasses import dataclass, field +from typing import Any, List, Optional +from unittest.mock import MagicMock, patch + +import pytest + +from agentspan.agents.testing.eval_runner import ( + CorrectnessEval, + EvalCase, + EvalCaseResult, + EvalCheckResult, + EvalSuiteResult, +) +from agentspan.agents.result import AgentResult, FinishReason, Status + + +# ── Minimal stubs ────────────────────────────────────────────────────────── + + +@dataclass +class StubAgent: + name: str = "stub-agent" + + +def _make_agent_result(status: str = "COMPLETED") -> AgentResult: + return AgentResult( + execution_id="exec-123", + status=Status(status), + finish_reason=FinishReason.STOP, + output="Test output", + messages=[], + events=[], + tool_calls=[], + ) + + +class StubRuntime: + """Minimal runtime that records calls and returns a canned AgentResult.""" + + def __init__(self, result: Optional[AgentResult] = None): + self._result = result or _make_agent_result() + self.calls: list = [] + self._posted_payloads: list = [] + + def run(self, agent: Any, prompt: str, *, session_id: str = "", **kwargs) -> AgentResult: + self.calls.append({"agent": agent, "prompt": prompt, "session_id": session_id}) + return self._result + + def _post_eval_run(self, payload: dict) -> None: + self._posted_payloads.append(payload) + + +# ── EvalCheckResult ──────────────────────────────────────────────────────── + + +class TestEvalCheckResultToDict: + def test_basic_fields(self): + check = EvalCheckResult(check="status", passed=True) + d = check.to_dict() + assert d["check"] == "status" + assert d["passed"] is True + assert d["message"] == "" + assert "score" not in d + assert "reasoning" not in d + + def test_failed_with_message(self): + check = EvalCheckResult(check="tool_used:lookup", passed=False, message="Tool not used") + d = check.to_dict() + assert d["passed"] is False + assert d["message"] == "Tool not used" + + def test_semantic_score_and_reasoning_included_when_set(self): + check = EvalCheckResult( + check="assert_output_satisfies", + passed=True, + score=0.85, + reasoning="The response addressed the issue clearly.", + ) + d = check.to_dict() + assert d["score"] == pytest.approx(0.85) + assert d["reasoning"] == "The response addressed the issue clearly." + + def test_score_none_not_in_dict(self): + check = EvalCheckResult(check="status", passed=True, score=None) + assert "score" not in check.to_dict() + + def test_reasoning_none_not_in_dict(self): + check = EvalCheckResult(check="status", passed=True, reasoning=None) + assert "reasoning" not in check.to_dict() + + +# ── EvalCaseResult ───────────────────────────────────────────────────────── + + +class TestEvalCaseResultToDict: + def test_basic_fields(self): + case = EvalCaseResult(name="my_case", passed=True, agent_name="my-agent") + d = case.to_dict() + assert d["name"] == "my_case" + assert d["passed"] is True + assert d["agentName"] == "my-agent" + assert d["checks"] == [] + + def test_checks_serialized(self): + case = EvalCaseResult( + name="c", + passed=False, + checks=[EvalCheckResult(check="status", passed=True)], + ) + d = case.to_dict() + assert len(d["checks"]) == 1 + assert d["checks"][0]["check"] == "status" + + def test_error_included(self): + case = EvalCaseResult(name="c", passed=False, error="Timeout") + assert case.to_dict()["error"] == "Timeout" + + +# ── EvalSuiteResult ──────────────────────────────────────────────────────── + + +class TestEvalSuiteResultToDict: + def test_basic_structure(self): + suite = EvalSuiteResult( + eval_run_id="run-abc", + agent_name="my-agent", + timestamp="2025-01-01T00:00:00Z", + cases=[ + EvalCaseResult(name="c1", passed=True), + EvalCaseResult(name="c2", passed=False), + ], + ) + d = suite.to_dict() + assert d["id"] == "run-abc" + assert d["agentName"] == "my-agent" + assert d["totalCases"] == 2 + assert d["passedCases"] == 1 + assert len(d["cases"]) == 2 + + def test_empty_suite(self): + suite = EvalSuiteResult() + d = suite.to_dict() + assert d["totalCases"] == 0 + assert d["passedCases"] == 0 + assert d["cases"] == [] + + +# ── CorrectnessEval — eval session tagging (Gap 1) ───────────────────────── + + +class TestEvalRunTagging: + def test_run_passes_eval_session_id_to_runtime(self): + agent = StubAgent("billing-agent") + runtime = StubRuntime() + ev = CorrectnessEval(runtime) + + ev.run([EvalCase(name="c1", agent=agent, prompt="Hello")]) + + assert len(runtime.calls) == 1 + session_id = runtime.calls[0]["session_id"] + assert session_id.startswith("eval:"), f"session_id should start with 'eval:', got: {session_id!r}" + + def test_all_cases_share_same_eval_session_id(self): + agent = StubAgent() + runtime = StubRuntime() + ev = CorrectnessEval(runtime) + + ev.run([ + EvalCase(name="c1", agent=agent, prompt="First"), + EvalCase(name="c2", agent=agent, prompt="Second"), + ]) + + session_ids = [c["session_id"] for c in runtime.calls] + assert len(set(session_ids)) == 1, "All cases in a suite should use the same eval session_id" + assert session_ids[0].startswith("eval:") + + def test_different_suite_runs_get_different_session_ids(self): + agent = StubAgent() + runtime = StubRuntime() + ev = CorrectnessEval(runtime) + + ev.run([EvalCase(name="c1", agent=agent, prompt="A")]) + ev.run([EvalCase(name="c2", agent=agent, prompt="B")]) + + ids = [c["session_id"] for c in runtime.calls] + assert ids[0] != ids[1], "Different eval.run() calls should produce different session_ids" + + +# ── CorrectnessEval — result POSTed to server (Gap 2) ───────────────────── + + +class TestEvalResultPosted: + def test_post_eval_run_called_after_run(self): + agent = StubAgent("my-agent") + runtime = StubRuntime() + ev = CorrectnessEval(runtime) + + ev.run([EvalCase(name="billing_case", agent=agent, prompt="I need a refund")]) + + assert len(runtime._posted_payloads) == 1 + payload = runtime._posted_payloads[0] + assert payload["agentName"] == "my-agent" + assert payload["totalCases"] == 1 + assert "id" in payload + assert "timestamp" in payload + + def test_post_eval_run_not_raised_when_runtime_has_no_method(self): + """If runtime doesn't have _post_eval_run, eval.run() should not raise.""" + + class MinimalRuntime: + def run(self, agent, prompt, **kw): + return _make_agent_result() + + agent = StubAgent() + ev = CorrectnessEval(MinimalRuntime()) + # Should not raise + result = ev.run([EvalCase(name="c", agent=agent, prompt="Hi")]) + assert result.total == 1 + + def test_post_eval_run_failure_doesnt_raise(self): + """A server POST failure must not propagate out of eval.run().""" + + class FailingRuntime: + def run(self, agent, prompt, **kw): + return _make_agent_result() + + def _post_eval_run(self, payload): + raise ConnectionError("Server unreachable") + + agent = StubAgent() + ev = CorrectnessEval(FailingRuntime()) + result = ev.run([EvalCase(name="c", agent=agent, prompt="Hi")]) + assert result.total == 1 # result still returned even when POST fails + + +# ── CorrectnessEval — suite metadata (Gap 2) ────────────────────────────── + + +class TestSuiteMetadata: + def test_suite_result_has_eval_run_id(self): + agent = StubAgent() + runtime = StubRuntime() + ev = CorrectnessEval(runtime) + + result = ev.run([EvalCase(name="c", agent=agent, prompt="Hi")]) + + assert result.eval_run_id, "eval_run_id should be set" + assert result.timestamp, "timestamp should be set" + + def test_suite_result_has_agent_name_from_case(self): + agent = StubAgent("special-agent") + runtime = StubRuntime() + ev = CorrectnessEval(runtime) + + result = ev.run([EvalCase(name="c", agent=agent, prompt="Hi")]) + + assert result.agent_name == "special-agent" + + def test_suite_tags_passed_through(self): + agent = StubAgent() + runtime = StubRuntime() + ev = CorrectnessEval(runtime) + + result = ev.run( + [EvalCase(name="c", agent=agent, prompt="Hi")], + suite_tags=["nightly", "billing"], + ) + + assert "nightly" in result.suite_tags + assert "billing" in result.suite_tags + + def test_name_strategy_ranby_serialized(self): + agent = StubAgent("my-agent") + runtime = StubRuntime() + ev = CorrectnessEval(runtime) + + result = ev.run( + [EvalCase(name="c", agent=agent, prompt="Hi")], + name="my_eval_v1", + strategy="react", + ran_by="ci_pipeline.py", + ) + + assert result.name == "my_eval_v1" + assert result.strategy == "react" + assert result.ran_by == "ci_pipeline.py" + + payload = runtime._posted_payloads[0] + assert payload["name"] == "my_eval_v1" + assert payload["strategy"] == "react" + assert payload["ranBy"] == "ci_pipeline.py" + + +# ── CorrectnessEval — tags filter ───────────────────────────────────────── + + +class TestTagsFilter: + def test_tags_filter_runs_only_matching_cases(self): + agent = StubAgent() + runtime = StubRuntime() + ev = CorrectnessEval(runtime) + + cases = [ + EvalCase(name="auth_case", agent=agent, prompt="Login", tags=["auth"]), + EvalCase(name="billing_case", agent=agent, prompt="Refund", tags=["billing"]), + EvalCase(name="both_case", agent=agent, prompt="Both", tags=["auth", "billing"]), + ] + + result = ev.run(cases, tags=["auth"]) + + ran_names = [c.name for c in result.cases] + assert "auth_case" in ran_names + assert "both_case" in ran_names + assert "billing_case" not in ran_names + assert result.total == 2 + + def test_tags_filter_with_no_match_runs_zero_cases(self): + agent = StubAgent() + runtime = StubRuntime() + ev = CorrectnessEval(runtime) + + cases = [EvalCase(name="c1", agent=agent, prompt="Hi", tags=["nightly"])] + + result = ev.run(cases, tags=["smoke"]) + + assert result.total == 0 + assert len(runtime.calls) == 0 + + def test_no_tags_filter_runs_all_cases(self): + agent = StubAgent() + runtime = StubRuntime() + ev = CorrectnessEval(runtime) + + cases = [ + EvalCase(name="c1", agent=agent, prompt="A", tags=["x"]), + EvalCase(name="c2", agent=agent, prompt="B", tags=["y"]), + EvalCase(name="c3", agent=agent, prompt="C"), + ] + + result = ev.run(cases) + + assert result.total == 3 + + +# ── agent_name not clobbered by expect_no_handoff_to loop ───────────────── + + +class TestAgentNamePreserved: + def test_agent_name_not_overwritten_by_no_handoff_loop(self): + """expect_no_handoff_to used a loop variable named agent_name that shadowed + the outer agent_name — causing EvalCaseResult.agent_name to be set to the + last entry of expect_no_handoff_to instead of the actual agent name.""" + agent = StubAgent("my-agent") + runtime = StubRuntime() + ev = CorrectnessEval(runtime) + + result = ev.run([ + EvalCase( + name="routing_case", + agent=agent, + prompt="Hi", + expect_no_handoff_to=["other-agent", "third-agent"], + ) + ]) + + assert result.cases[0].agent_name == "my-agent", ( + f"agent_name was corrupted to {result.cases[0].agent_name!r}" + ) diff --git a/server/src/main/java/dev/agentspan/runtime/controller/AgentController.java b/server/src/main/java/dev/agentspan/runtime/controller/AgentController.java index 23b9934bb..d62b324d0 100644 --- a/server/src/main/java/dev/agentspan/runtime/controller/AgentController.java +++ b/server/src/main/java/dev/agentspan/runtime/controller/AgentController.java @@ -137,8 +137,10 @@ public Map searchAgentExecutions( @RequestParam(required = false) String freeText, @RequestParam(required = false) String status, @RequestParam(required = false) String agentName, - @RequestParam(required = false) String sessionId) { - return agentService.searchAgentExecutions(start, size, sort, freeText, status, agentName, sessionId); + @RequestParam(required = false) String sessionId, + @RequestParam(defaultValue = "false") boolean includeEvalRuns) { + return agentService.searchAgentExecutions( + start, size, sort, freeText, status, agentName, sessionId, includeEvalRuns); } @GetMapping("/{name}") @@ -350,8 +352,9 @@ public SearchResult searchExecutionsRaw( @RequestParam(defaultValue = "20") int size, @RequestParam(defaultValue = "startTime:DESC") String sort, @RequestParam(required = false) String freeText, - @RequestParam(required = false) String query) { - return agentService.searchExecutionsRaw(start, size, sort, freeText, query); + @RequestParam(required = false) String query, + @RequestParam(defaultValue = "false") boolean includeEvalRuns) { + return agentService.searchExecutionsRaw(start, size, sort, freeText, query, includeEvalRuns); } // ── Bulk operations ───────────────────────────────────────────── diff --git a/server/src/main/java/dev/agentspan/runtime/eval/DatasetDto.java b/server/src/main/java/dev/agentspan/runtime/eval/DatasetDto.java new file mode 100644 index 000000000..d6f924fb2 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/eval/DatasetDto.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ +package dev.agentspan.runtime.eval; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DatasetDto { + private String name; + /** ISO-8601 UTC timestamp of last push. */ + private String updatedAt; + /** Username or script that last pushed this dataset. */ + private String pushedBy; + /** Number of cases in the dataset — populated on list responses. */ + private Integer caseCount; + /** Present only in detail responses. */ + private List cases; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class DatasetCaseDto { + private String name; + private String prompt; + private List assertions; + private List tags; + private String semanticCriteria; + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/eval/EvalCaseDto.java b/server/src/main/java/dev/agentspan/runtime/eval/EvalCaseDto.java new file mode 100644 index 000000000..f17601d48 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/eval/EvalCaseDto.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ +package dev.agentspan.runtime.eval; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class EvalCaseDto { + private String id; + private String evalRunId; + private String name; + private boolean passed; + private String error; + private String agentName; + private String model; + private List tags; + private String prompt; + private String output; + private List checks; +} diff --git a/server/src/main/java/dev/agentspan/runtime/eval/EvalCheckDto.java b/server/src/main/java/dev/agentspan/runtime/eval/EvalCheckDto.java new file mode 100644 index 000000000..6a4103935 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/eval/EvalCheckDto.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ +package dev.agentspan.runtime.eval; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class EvalCheckDto { + private String id; + private String evalCaseId; + private String check; + private boolean passed; + private String message; + private Double score; + private String reasoning; +} diff --git a/server/src/main/java/dev/agentspan/runtime/eval/EvalController.java b/server/src/main/java/dev/agentspan/runtime/eval/EvalController.java new file mode 100644 index 000000000..0541370e8 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/eval/EvalController.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ +package dev.agentspan.runtime.eval; + +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.*; + +import lombok.RequiredArgsConstructor; + +@Component +@RestController +@RequestMapping("/api/eval") +@RequiredArgsConstructor +public class EvalController { + + private final EvalService evalService; + + // ── Eval Runs ────────────────────────────────────────────────────── + + /** Submit an eval suite result from the SDK. */ + @PostMapping("/runs") + public EvalRunDto submitEvalRun(@RequestBody EvalRunDto dto) { + return evalService.saveEvalRun(dto); + } + + /** List eval runs (paginated), newest first. */ + @GetMapping("/runs") + public Map listEvalRuns( + @RequestParam(defaultValue = "0") int start, @RequestParam(defaultValue = "20") int size) { + return evalService.listEvalRuns(start, size); + } + + /** Get a single eval run with all case and check details. */ + @GetMapping("/runs/{id}") + public ResponseEntity getEvalRun(@PathVariable String id) { + try { + return ResponseEntity.ok(evalService.getEvalRun(id)); + } catch (NoSuchElementException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", e.getMessage())); + } + } + + // ── Datasets ─────────────────────────────────────────────────────── + + /** Push (upsert) a named dataset from the SDK. */ + @PostMapping("/datasets") + public ResponseEntity pushDataset(@RequestBody DatasetDto dto) { + evalService.pushDataset(dto); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + /** List all datasets (summary — name, updatedAt, pushedBy, caseCount). */ + @GetMapping("/datasets") + public List listDatasets() { + return evalService.listDatasets(); + } + + /** Get a single dataset with all cases. */ + @GetMapping("/datasets/{name}") + public ResponseEntity getDataset(@PathVariable String name) { + try { + return ResponseEntity.ok(evalService.getDataset(name)); + } catch (NoSuchElementException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", e.getMessage())); + } + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/eval/EvalRunDto.java b/server/src/main/java/dev/agentspan/runtime/eval/EvalRunDto.java new file mode 100644 index 000000000..bb12d60bf --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/eval/EvalRunDto.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ +package dev.agentspan.runtime.eval; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class EvalRunDto { + private String id; + private String agentName; + private String timestamp; + private int totalCases; + private int passedCases; + private List tags; + private String createdBy; + private String name; + private String strategy; + private String ranBy; + /** Present only in detail responses. */ + private List cases; +} diff --git a/server/src/main/java/dev/agentspan/runtime/eval/EvalSchemaConfig.java b/server/src/main/java/dev/agentspan/runtime/eval/EvalSchemaConfig.java new file mode 100644 index 000000000..4e1f2b285 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/eval/EvalSchemaConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ +package dev.agentspan.runtime.eval; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.datasource.init.DataSourceInitializer; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; + +/** + * Initializes eval observability tables on the shared credential DataSource. + * + *

Reuses the @Primary DataSource created by CredentialDataSourceConfig so + * all AgentSpan tables live in the same SQLite/PostgreSQL database file.

+ */ +@Configuration +public class EvalSchemaConfig { + + @Value("${spring.datasource.url:jdbc:sqlite:agent-runtime.db}") + private String datasourceUrl; + + private boolean isPostgres() { + return datasourceUrl != null && datasourceUrl.startsWith("jdbc:postgresql"); + } + + @Bean + public DataSourceInitializer evalSchemaInitializer(DataSource dataSource) { + String schemaFile = isPostgres() ? "schema-eval-postgres.sql" : "schema-eval.sql"; + DataSourceInitializer initializer = new DataSourceInitializer(); + initializer.setDataSource(dataSource); + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); + populator.addScript(new ClassPathResource(schemaFile)); + populator.setContinueOnError(true); + initializer.setDatabasePopulator(populator); + return initializer; + } + + @Bean("evalJdbc") + public NamedParameterJdbcTemplate evalJdbc(DataSource dataSource) { + return new NamedParameterJdbcTemplate(dataSource); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/eval/EvalService.java b/server/src/main/java/dev/agentspan/runtime/eval/EvalService.java new file mode 100644 index 000000000..890233f06 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/eval/EvalService.java @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ +package dev.agentspan.runtime.eval; + +import java.time.Instant; +import java.util.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import dev.agentspan.runtime.auth.RequestContextHolder; + +@Service +public class EvalService { + + private static final Logger log = LoggerFactory.getLogger(EvalService.class); + private static final TypeReference> STRING_LIST = new TypeReference<>() {}; + private static final TypeReference> CASE_LIST = new TypeReference<>() {}; + + private final NamedParameterJdbcTemplate jdbc; + private final ObjectMapper mapper; + + public EvalService(@Qualifier("evalJdbc") NamedParameterJdbcTemplate jdbc, ObjectMapper mapper) { + this.jdbc = jdbc; + this.mapper = mapper; + } + + // ── Eval Runs ────────────────────────────────────────────────────── + + @Transactional + public EvalRunDto saveEvalRun(EvalRunDto dto) { + String runId = dto.getId() != null ? dto.getId() : UUID.randomUUID().toString(); + String timestamp = + dto.getTimestamp() != null ? dto.getTimestamp() : Instant.now().toString(); + String createdBy = + RequestContextHolder.get().map(ctx -> ctx.getUser().getEmail()).orElse(null); + + // agent_name is nullable — multi-agent evals may not have a single canonical agent + String agentName = dto.getAgentName() != null ? dto.getAgentName() : ""; + + String tagsJson = toJson(dto.getTags()); + + jdbc.update( + "INSERT OR REPLACE INTO eval_runs" + + " (id, agent_name, timestamp, total_cases, passed_cases, tags, created_by, name, strategy, ran_by)" + + " VALUES (:id, :agentName, :timestamp, :totalCases, :passedCases, :tags, :createdBy, :name, :strategy, :ranBy)", + new MapSqlParameterSource() + .addValue("id", runId) + .addValue("agentName", agentName) + .addValue("timestamp", timestamp) + .addValue("totalCases", dto.getTotalCases()) + .addValue("passedCases", dto.getPassedCases()) + .addValue("tags", tagsJson) + .addValue("createdBy", createdBy) + .addValue("name", dto.getName()) + .addValue("strategy", dto.getStrategy()) + .addValue("ranBy", dto.getRanBy())); + + if (dto.getCases() != null) { + for (EvalCaseDto caseDto : dto.getCases()) { + saveCaseWithChecks(runId, caseDto); + } + } + + log.debug("Saved eval run {} ({}/{} passed)", runId, dto.getPassedCases(), dto.getTotalCases()); + return dto.toBuilder() + .id(runId) + .timestamp(timestamp) + .createdBy(createdBy) + .build(); + } + + private void saveCaseWithChecks(String runId, EvalCaseDto caseDto) { + String caseId = + caseDto.getId() != null ? caseDto.getId() : UUID.randomUUID().toString(); + + jdbc.update( + "INSERT OR REPLACE INTO eval_cases" + + " (id, eval_run_id, case_name, passed, error, agent_name, model, tags, prompt, output)" + + " VALUES (:id, :evalRunId, :caseName, :passed, :error, :agentName, :model, :tags, :prompt, :output)", + new MapSqlParameterSource() + .addValue("id", caseId) + .addValue("evalRunId", runId) + .addValue("caseName", caseDto.getName()) + .addValue("passed", caseDto.isPassed() ? 1 : 0) + .addValue("error", caseDto.getError()) + .addValue("agentName", caseDto.getAgentName()) + .addValue("model", caseDto.getModel()) + .addValue("tags", toJson(caseDto.getTags())) + .addValue("prompt", caseDto.getPrompt()) + .addValue("output", caseDto.getOutput())); + + if (caseDto.getChecks() != null) { + for (EvalCheckDto check : caseDto.getChecks()) { + saveCheck(caseId, check); + } + } + } + + private void saveCheck(String caseId, EvalCheckDto check) { + String checkId = + check.getId() != null ? check.getId() : UUID.randomUUID().toString(); + jdbc.update( + "INSERT OR REPLACE INTO eval_checks (id, eval_case_id, check_name, passed, message, score, reasoning)" + + " VALUES (:id, :evalCaseId, :checkName, :passed, :message, :score, :reasoning)", + new MapSqlParameterSource() + .addValue("id", checkId) + .addValue("evalCaseId", caseId) + .addValue("checkName", check.getCheck()) + .addValue("passed", check.isPassed() ? 1 : 0) + .addValue("message", check.getMessage()) + .addValue("score", check.getScore()) + .addValue("reasoning", check.getReasoning())); + } + + public Map listEvalRuns(int start, int size) { + long total = Optional.ofNullable( + jdbc.queryForObject("SELECT COUNT(*) FROM eval_runs", new MapSqlParameterSource(), Long.class)) + .orElse(0L); + + List results = jdbc.query( + "SELECT id, agent_name, timestamp, total_cases, passed_cases, tags, created_by, name, strategy, ran_by" + + " FROM eval_runs ORDER BY timestamp DESC LIMIT :size OFFSET :start", + new MapSqlParameterSource().addValue("size", size).addValue("start", start), + (rs, rowNum) -> EvalRunDto.builder() + .id(rs.getString("id")) + .agentName(rs.getString("agent_name")) + .timestamp(rs.getString("timestamp")) + .totalCases(rs.getInt("total_cases")) + .passedCases(rs.getInt("passed_cases")) + .tags(fromJson(rs.getString("tags"))) + .createdBy(rs.getString("created_by")) + .name(rs.getString("name")) + .strategy(rs.getString("strategy")) + .ranBy(rs.getString("ran_by")) + .build()); + + Map response = new LinkedHashMap<>(); + response.put("totalHits", total); + response.put("results", results); + return response; + } + + public EvalRunDto getEvalRun(String id) { + List runs = jdbc.query( + "SELECT id, agent_name, timestamp, total_cases, passed_cases, tags, created_by, name, strategy, ran_by" + + " FROM eval_runs WHERE id = :id", + new MapSqlParameterSource("id", id), + (rs, rowNum) -> EvalRunDto.builder() + .id(rs.getString("id")) + .agentName(rs.getString("agent_name")) + .timestamp(rs.getString("timestamp")) + .totalCases(rs.getInt("total_cases")) + .passedCases(rs.getInt("passed_cases")) + .tags(fromJson(rs.getString("tags"))) + .createdBy(rs.getString("created_by")) + .name(rs.getString("name")) + .strategy(rs.getString("strategy")) + .ranBy(rs.getString("ran_by")) + .build()); + + if (runs.isEmpty()) { + throw new NoSuchElementException("Eval run not found: " + id); + } + + EvalRunDto run = runs.get(0); + List cases = loadCasesForRun(id); + return run.toBuilder().cases(cases).build(); + } + + private List loadCasesForRun(String runId) { + // Load cases first so the result set (and its connection) is fully consumed + // before we issue the nested checks query. With a single-connection SQLite pool + // calling loadChecksForCase() inside the RowMapper would deadlock. + List cases = jdbc.query( + "SELECT id, case_name, passed, error, agent_name, model, tags, prompt, output" + + " FROM eval_cases WHERE eval_run_id = :runId", + new MapSqlParameterSource("runId", runId), + (rs, rowNum) -> EvalCaseDto.builder() + .id(rs.getString("id")) + .evalRunId(runId) + .name(rs.getString("case_name")) + .passed(rs.getInt("passed") == 1) + .error(rs.getString("error")) + .agentName(rs.getString("agent_name")) + .model(rs.getString("model")) + .tags(fromJson(rs.getString("tags"))) + .prompt(rs.getString("prompt")) + .output(rs.getString("output")) + .build()); + + // Now enrich each case with its checks (connection is free between each call) + return cases.stream() + .map(c -> c.toBuilder().checks(loadChecksForCase(c.getId())).build()) + .toList(); + } + + private List loadChecksForCase(String caseId) { + return jdbc.query( + "SELECT id, check_name, passed, message, score, reasoning FROM eval_checks WHERE eval_case_id = :caseId", + new MapSqlParameterSource("caseId", caseId), + (rs, rowNum) -> { + double scoreVal = rs.getDouble("score"); + Double score = rs.wasNull() ? null : scoreVal; + return EvalCheckDto.builder() + .id(rs.getString("id")) + .evalCaseId(caseId) + .check(rs.getString("check_name")) + .passed(rs.getInt("passed") == 1) + .message(rs.getString("message")) + .score(score) + .reasoning(rs.getString("reasoning")) + .build(); + }); + } + + // ── Datasets ─────────────────────────────────────────────────────── + + public void pushDataset(DatasetDto dto) { + String updatedAt = Instant.now().toString(); + String casesJson = toJson(dto.getCases()); + int caseCount = dto.getCases() != null ? dto.getCases().size() : 0; + + jdbc.update( + "INSERT OR REPLACE INTO eval_datasets (name, cases_json, updated_at, pushed_by, case_count)" + + " VALUES (:name, :casesJson, :updatedAt, :pushedBy, :caseCount)", + new MapSqlParameterSource() + .addValue("name", dto.getName()) + .addValue("casesJson", casesJson) + .addValue("updatedAt", updatedAt) + .addValue("pushedBy", dto.getPushedBy()) + .addValue("caseCount", caseCount)); + + log.debug("Pushed dataset '{}' ({} cases)", dto.getName(), caseCount); + } + + /** List all datasets — metadata only, does NOT load or parse cases JSON. */ + public List listDatasets() { + return jdbc.query( + "SELECT name, updated_at, pushed_by, case_count FROM eval_datasets ORDER BY name", + new MapSqlParameterSource(), + (rs, rowNum) -> DatasetDto.builder() + .name(rs.getString("name")) + .updatedAt(rs.getString("updated_at")) + .pushedBy(rs.getString("pushed_by")) + .caseCount(rs.getInt("case_count")) + .build()); + } + + public DatasetDto getDataset(String name) { + List results = jdbc.query( + "SELECT name, cases_json, updated_at, pushed_by, case_count FROM eval_datasets WHERE name = :name", + new MapSqlParameterSource("name", name), + (rs, rowNum) -> DatasetDto.builder() + .name(rs.getString("name")) + .updatedAt(rs.getString("updated_at")) + .pushedBy(rs.getString("pushed_by")) + .caseCount(rs.getInt("case_count")) + .cases(parseCases(rs.getString("cases_json"))) + .build()); + + if (results.isEmpty()) { + throw new NoSuchElementException("Dataset not found: " + name); + } + return results.get(0); + } + + // ── JSON helpers ─────────────────────────────────────────────────── + + private String toJson(Object obj) { + if (obj == null) return null; + try { + return mapper.writeValueAsString(obj); + } catch (Exception e) { + log.warn("Failed to serialize object to JSON: {}", e.getMessage()); + return null; + } + } + + private List fromJson(String json) { + if (json == null || json.isBlank()) return List.of(); + try { + return mapper.readValue(json, STRING_LIST); + } catch (Exception e) { + log.warn("Failed to deserialize JSON tags: {}", e.getMessage()); + return List.of(); + } + } + + private List parseCases(String json) { + if (json == null || json.isBlank()) return List.of(); + try { + return mapper.readValue(json, CASE_LIST); + } catch (Exception e) { + log.warn("Failed to deserialize dataset cases JSON: {}", e.getMessage()); + return List.of(); + } + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/normalizer/SkillNormalizer.java b/server/src/main/java/dev/agentspan/runtime/normalizer/SkillNormalizer.java index 1633e1e17..77cb029d0 100644 --- a/server/src/main/java/dev/agentspan/runtime/normalizer/SkillNormalizer.java +++ b/server/src/main/java/dev/agentspan/runtime/normalizer/SkillNormalizer.java @@ -89,6 +89,18 @@ public AgentConfig normalize(Map rawConfig) { orchestrator.setModel(model); orchestrator.setInstructions(body); orchestrator.setDescription(description); + Integer maxTurns = asInteger(rawConfig.get("maxTurns")); + if (maxTurns != null && maxTurns > 0) { + orchestrator.setMaxTurns(maxTurns); + } + Integer maxTokens = asInteger(rawConfig.get("maxTokens")); + if (maxTokens != null && maxTokens > 0) { + orchestrator.setMaxTokens(maxTokens); + } + Integer timeoutSeconds = asInteger(rawConfig.get("timeoutSeconds")); + if (timeoutSeconds != null && timeoutSeconds >= 0) { + orchestrator.setTimeoutSeconds(timeoutSeconds); + } // Pass through metadata if (frontmatter.containsKey("metadata")) { @@ -368,4 +380,18 @@ static String slugify(String text) { // Remove leading/trailing hyphens return slug.replaceAll("^-+|-+$", ""); } + + private Integer asInteger(Object value) { + if (value instanceof Number) { + return ((Number) value).intValue(); + } + if (value instanceof String) { + try { + return Integer.parseInt((String) value); + } catch (NumberFormatException ignored) { + return null; + } + } + return null; + } } diff --git a/server/src/main/java/dev/agentspan/runtime/service/AgentService.java b/server/src/main/java/dev/agentspan/runtime/service/AgentService.java index d74253329..2b98e9075 100644 --- a/server/src/main/java/dev/agentspan/runtime/service/AgentService.java +++ b/server/src/main/java/dev/agentspan/runtime/service/AgentService.java @@ -11,6 +11,7 @@ import java.time.temporal.ChronoUnit; import java.util.*; import java.util.Optional; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -373,7 +374,14 @@ public List listAgents() { * Search agent executions with optional filters. */ public Map searchAgentExecutions( - int start, int size, String sort, String freeText, String status, String agentName, String sessionId) { + int start, + int size, + String sort, + String freeText, + String status, + String agentName, + String sessionId, + boolean includeEvalRuns) { // Determine which workflow types to query List workflowNames; if (agentName != null && !agentName.isEmpty()) { @@ -406,7 +414,9 @@ public Map searchAgentExecutions( SearchResult searchResult = workflowService.searchWorkflows(start, size, sort, searchText, query.toString()); - List results = searchResult.getResults().stream() + List rawPage = searchResult.getResults(); + List results = rawPage.stream() + .filter(ws -> includeEvalRuns || !isEvalRun(ws.getInput())) .map(ws -> AgentExecutionSummary.builder() .executionId(ws.getWorkflowId()) .agentName(ws.getWorkflowType()) @@ -422,12 +432,39 @@ public Map searchAgentExecutions( .build()) .collect(Collectors.toList()); + // Adjust totalHits to account for eval runs filtered out on this page. + // Exact cross-page total is not available without a separate query, so we + // subtract the per-page filtered count as a best-effort approximation. + long filteredOut = rawPage.size() - results.size(); + long totalHits = Math.max(0, searchResult.getTotalHits() - filteredOut); + Map response = new LinkedHashMap<>(); - response.put("totalHits", searchResult.getTotalHits()); + response.put("totalHits", totalHits); response.put("results", results); return response; } + /** + * Returns true when the workflow input indicates an eval run. + * + *

Conductor's WorkflowSummary serializes input via SummaryUtil which uses either JSON + * ({@code "key":"value"}) or Map.toString() ({@code key=value}) depending on config. Both + * formats are checked to ensure eval runs are detected regardless of Conductor configuration. + */ + private static final Pattern EVAL_SESSION_PATTERN = Pattern.compile("\"session_id\"\\s*:\\s*\"eval:"); + + private boolean isEvalRun(String input) { + if (input == null) return false; + // JSON format: {"_eval_run":true, ...} + if (input.contains("\"_eval_run\":true")) return true; + // Map.toString() format: {_eval_run=true, ...} + if (input.contains("_eval_run=true")) return true; + // JSON format: {"session_id": "eval:..."} + if (EVAL_SESSION_PATTERN.matcher(input).find()) return true; + // Map.toString() format: {session_id=eval:...} + return input.contains("session_id=eval:"); + } + /** * Get detailed execution status for a single agent execution. */ @@ -1400,8 +1437,15 @@ public TaskListResponse getExecutionTasks(String executionId, String status, int } public SearchResult searchExecutionsRaw( - int start, int size, String sort, String freeText, String query) { - return workflowService.searchWorkflows(start, size, sort, freeText, query); + int start, int size, String sort, String freeText, String query, boolean includeEvalRuns) { + SearchResult result = workflowService.searchWorkflows(start, size, sort, freeText, query); + if (!includeEvalRuns) { + List filtered = result.getResults().stream() + .filter(ws -> !isEvalRun(ws.getInput())) + .collect(Collectors.toList()); + return new SearchResult<>(result.getTotalHits(), filtered); + } + return result; } public WorkflowDef getAgentDefinition(String name, Integer version) { diff --git a/server/src/main/resources/schema-eval-postgres.sql b/server/src/main/resources/schema-eval-postgres.sql new file mode 100644 index 000000000..5fed42b33 --- /dev/null +++ b/server/src/main/resources/schema-eval-postgres.sql @@ -0,0 +1,64 @@ +-- schema-eval-postgres.sql +-- Agentspan eval observability tables (PostgreSQL variant). + +CREATE TABLE IF NOT EXISTS eval_runs ( + id TEXT PRIMARY KEY, + agent_name TEXT, + timestamp TEXT NOT NULL, + total_cases INTEGER NOT NULL DEFAULT 0, + passed_cases INTEGER NOT NULL DEFAULT 0, + tags TEXT, + created_by TEXT, + name TEXT, + strategy TEXT, + ran_by TEXT +); + +-- migrations for existing installs +ALTER TABLE eval_runs ADD COLUMN IF NOT EXISTS name TEXT; +ALTER TABLE eval_runs ADD COLUMN IF NOT EXISTS strategy TEXT; +ALTER TABLE eval_runs ADD COLUMN IF NOT EXISTS ran_by TEXT; + +CREATE TABLE IF NOT EXISTS eval_cases ( + id TEXT PRIMARY KEY, + eval_run_id TEXT NOT NULL, + case_name TEXT NOT NULL, + passed INTEGER NOT NULL DEFAULT 0, + error TEXT, + agent_name TEXT, + model TEXT, + tags TEXT, + prompt TEXT, + output TEXT +); + +-- migrations for existing installs +ALTER TABLE eval_cases ADD COLUMN IF NOT EXISTS prompt TEXT; +ALTER TABLE eval_cases ADD COLUMN IF NOT EXISTS output TEXT; + +CREATE TABLE IF NOT EXISTS eval_checks ( + id TEXT PRIMARY KEY, + eval_case_id TEXT NOT NULL, + check_name TEXT NOT NULL, + passed INTEGER NOT NULL DEFAULT 0, + message TEXT, + score DOUBLE PRECISION, + reasoning TEXT +); + +CREATE TABLE IF NOT EXISTS eval_datasets ( + name TEXT PRIMARY KEY, + cases_json TEXT NOT NULL, + updated_at TEXT NOT NULL, + pushed_by TEXT, + case_count INTEGER NOT NULL DEFAULT 0 +); + +-- migrations for existing installs +ALTER TABLE eval_datasets ADD COLUMN IF NOT EXISTS pushed_by TEXT; +ALTER TABLE eval_datasets ADD COLUMN IF NOT EXISTS case_count INTEGER NOT NULL DEFAULT 0; + +-- indices for FK lookup performance +CREATE INDEX IF NOT EXISTS idx_eval_cases_run_id ON eval_cases(eval_run_id); +CREATE INDEX IF NOT EXISTS idx_eval_checks_case_id ON eval_checks(eval_case_id); +CREATE INDEX IF NOT EXISTS idx_eval_runs_timestamp ON eval_runs(timestamp DESC); diff --git a/server/src/main/resources/schema-eval.sql b/server/src/main/resources/schema-eval.sql new file mode 100644 index 000000000..cac6c1b0c --- /dev/null +++ b/server/src/main/resources/schema-eval.sql @@ -0,0 +1,65 @@ +-- schema-eval.sql +-- Agentspan eval observability tables. Created on startup via EvalSchemaConfig. +-- SQLite-compatible DDL — IF NOT EXISTS guards make this idempotent. + +CREATE TABLE IF NOT EXISTS eval_runs ( + id TEXT PRIMARY KEY, -- UUID + agent_name TEXT, + timestamp TEXT NOT NULL, -- ISO-8601 UTC + total_cases INTEGER NOT NULL DEFAULT 0, + passed_cases INTEGER NOT NULL DEFAULT 0, + tags TEXT, -- JSON array of strings + created_by TEXT, + name TEXT, -- user-defined run name (e.g. "eval_handoff_v2") + strategy TEXT, -- orchestration strategy + ran_by TEXT -- script filename or "UI" +); + +-- migrations: add columns for existing installs (errors silently ignored via continueOnError) +ALTER TABLE eval_runs ADD COLUMN name TEXT; +ALTER TABLE eval_runs ADD COLUMN strategy TEXT; +ALTER TABLE eval_runs ADD COLUMN ran_by TEXT; + +CREATE TABLE IF NOT EXISTS eval_cases ( + id TEXT PRIMARY KEY, -- UUID + eval_run_id TEXT NOT NULL, -- FK → eval_runs.id + case_name TEXT NOT NULL, + passed INTEGER NOT NULL DEFAULT 0, -- 0=false, 1=true + error TEXT, + agent_name TEXT, + model TEXT, + tags TEXT, -- JSON array of strings + prompt TEXT, -- original prompt sent to agent + output TEXT -- agent response text +); + +-- migrations +ALTER TABLE eval_cases ADD COLUMN prompt TEXT; +ALTER TABLE eval_cases ADD COLUMN output TEXT; + +CREATE TABLE IF NOT EXISTS eval_checks ( + id TEXT PRIMARY KEY, -- UUID + eval_case_id TEXT NOT NULL, -- FK → eval_cases.id + check_name TEXT NOT NULL, + passed INTEGER NOT NULL DEFAULT 0, + message TEXT, + score REAL, -- semantic score 0-1; null for deterministic checks + reasoning TEXT -- LLM judge reasoning; null for deterministic checks +); + +CREATE TABLE IF NOT EXISTS eval_datasets ( + name TEXT PRIMARY KEY, -- user-defined unique dataset name + cases_json TEXT NOT NULL, -- JSON array of case objects + updated_at TEXT NOT NULL, -- ISO-8601 UTC + pushed_by TEXT, -- username / script that pushed this dataset + case_count INTEGER NOT NULL DEFAULT 0 -- pre-computed case count for fast list queries +); + +-- migrations +ALTER TABLE eval_datasets ADD COLUMN pushed_by TEXT; +ALTER TABLE eval_datasets ADD COLUMN case_count INTEGER NOT NULL DEFAULT 0; + +-- indices for FK lookup performance +CREATE INDEX IF NOT EXISTS idx_eval_cases_run_id ON eval_cases(eval_run_id); +CREATE INDEX IF NOT EXISTS idx_eval_checks_case_id ON eval_checks(eval_case_id); +CREATE INDEX IF NOT EXISTS idx_eval_runs_timestamp ON eval_runs(timestamp DESC); diff --git a/server/src/test/java/dev/agentspan/runtime/eval/EvalServiceTest.java b/server/src/test/java/dev/agentspan/runtime/eval/EvalServiceTest.java new file mode 100644 index 000000000..8f0313fb1 --- /dev/null +++ b/server/src/test/java/dev/agentspan/runtime/eval/EvalServiceTest.java @@ -0,0 +1,246 @@ +package dev.agentspan.runtime.eval; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.test.context.ActiveProfiles; + +import dev.agentspan.runtime.AgentRuntime; + +@SpringBootTest(classes = AgentRuntime.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@ActiveProfiles("test") +class EvalServiceTest { + + @Autowired + private EvalService evalService; + + @Autowired + @Qualifier("evalJdbc") + private NamedParameterJdbcTemplate jdbc; + + // Unique prefix per test class execution — prevents accidentally deleting unrelated rows + // in a shared database if the test profile is misconfigured. + private static final String P = + "evaltest-" + java.util.UUID.randomUUID().toString().substring(0, 8) + "-"; + + @BeforeEach + void cleanup() { + jdbc.update( + "DELETE FROM eval_checks WHERE eval_case_id IN (SELECT id FROM eval_cases WHERE eval_run_id LIKE :prefix)", + Map.of("prefix", P + "%")); + jdbc.update("DELETE FROM eval_cases WHERE eval_run_id LIKE :prefix", Map.of("prefix", P + "%")); + jdbc.update("DELETE FROM eval_runs WHERE id LIKE :prefix", Map.of("prefix", P + "%")); + jdbc.update("DELETE FROM eval_datasets WHERE name LIKE :prefix", Map.of("prefix", P + "%")); + } + + private String id(String suffix) { + return P + suffix; + } + + // ── Eval Runs ─────────────────────────────────────────────────── + + @Test + void saveEvalRun_persistsAndCanBeRetrieved() { + EvalRunDto dto = EvalRunDto.builder() + .id(id("run-1")) + .agentName("billing-agent") + .timestamp("2025-01-01T00:00:00Z") + .totalCases(2) + .passedCases(1) + .build(); + + EvalRunDto saved = evalService.saveEvalRun(dto); + + assertThat(saved.getId()).isEqualTo(id("run-1")); + assertThat(saved.getAgentName()).isEqualTo("billing-agent"); + assertThat(saved.getTotalCases()).isEqualTo(2); + assertThat(saved.getPassedCases()).isEqualTo(1); + } + + @Test + void getEvalRun_returnsNotFound_forMissingId() { + assertThatThrownBy(() -> evalService.getEvalRun("does-not-exist")) + .isInstanceOf(NoSuchElementException.class) + .hasMessageContaining("does-not-exist"); + } + + @Test + void saveEvalRun_withCasesAndChecks_roundTrips() { + EvalCheckDto check1 = + EvalCheckDto.builder().check("status").passed(true).message("").build(); + EvalCheckDto check2 = EvalCheckDto.builder() + .check("output_contains:'refund'") + .passed(false) + .message("Output did not contain 'refund'") + .build(); + EvalCheckDto semanticCheck = EvalCheckDto.builder() + .check("assert_output_satisfies") + .passed(true) + .score(0.87) + .reasoning("Response accurately addressed the issue") + .build(); + + EvalCaseDto caseDto = EvalCaseDto.builder() + .name("billing_routes_correctly") + .passed(false) + .agentName("billing-agent") + .checks(List.of(check1, check2, semanticCheck)) + .build(); + + EvalRunDto dto = EvalRunDto.builder() + .id(id("run-2")) + .agentName("billing-agent") + .timestamp("2025-01-01T00:00:00Z") + .totalCases(1) + .passedCases(0) + .cases(List.of(caseDto)) + .build(); + + evalService.saveEvalRun(dto); + + EvalRunDto retrieved = evalService.getEvalRun(id("run-2")); + + assertThat(retrieved.getCases()).hasSize(1); + EvalCaseDto retrievedCase = retrieved.getCases().get(0); + assertThat(retrievedCase.getName()).isEqualTo("billing_routes_correctly"); + assertThat(retrievedCase.isPassed()).isFalse(); + assertThat(retrievedCase.getChecks()).hasSize(3); + + EvalCheckDto retrievedSemantic = retrievedCase.getChecks().stream() + .filter(c -> c.getScore() != null) + .findFirst() + .orElseThrow(); + assertThat(retrievedSemantic.getScore()).isCloseTo(0.87, org.assertj.core.data.Offset.offset(0.01)); + assertThat(retrievedSemantic.getReasoning()).isEqualTo("Response accurately addressed the issue"); + } + + @Test + void listEvalRuns_returnsAll_inDescendingTimestampOrder() { + evalService.saveEvalRun(EvalRunDto.builder() + .id(id("run-a")) + .agentName("agent-a") + .timestamp("2025-01-01T00:00:00Z") + .totalCases(1) + .passedCases(1) + .build()); + evalService.saveEvalRun(EvalRunDto.builder() + .id(id("run-b")) + .agentName("agent-b") + .timestamp("2025-02-01T00:00:00Z") + .totalCases(2) + .passedCases(0) + .build()); + + Map result = evalService.listEvalRuns(0, 50); + + @SuppressWarnings("unchecked") + List runs = (List) result.get("results"); + List ids = runs.stream().map(EvalRunDto::getId).toList(); + + assertThat(ids).contains(id("run-a"), id("run-b")); + // Newer timestamp should come first + int idxB = ids.indexOf(id("run-b")); + int idxA = ids.indexOf(id("run-a")); + assertThat(idxB).isLessThan(idxA); + } + + // ── Datasets ──────────────────────────────────────────────────── + + @Test + void pushDataset_persistsAndCanBeRetrieved() { + DatasetDto.DatasetCaseDto c1 = DatasetDto.DatasetCaseDto.builder() + .name("billing_case") + .prompt("I need a refund") + .assertions(List.of("handoff_to:billing")) + .build(); + + DatasetDto dto = + DatasetDto.builder().name(id("dataset-1")).cases(List.of(c1)).build(); + + evalService.pushDataset(dto); + + DatasetDto retrieved = evalService.getDataset(id("dataset-1")); + + assertThat(retrieved.getName()).isEqualTo(id("dataset-1")); + assertThat(retrieved.getCases()).hasSize(1); + assertThat(retrieved.getCases().get(0).getPrompt()).isEqualTo("I need a refund"); + assertThat(retrieved.getCases().get(0).getAssertions()).contains("handoff_to:billing"); + } + + @Test + void pushDataset_storesCaseCount() { + DatasetDto dto = DatasetDto.builder() + .name(id("dataset-count")) + .cases(List.of( + DatasetDto.DatasetCaseDto.builder() + .name("c1") + .prompt("p1") + .build(), + DatasetDto.DatasetCaseDto.builder() + .name("c2") + .prompt("p2") + .build(), + DatasetDto.DatasetCaseDto.builder() + .name("c3") + .prompt("p3") + .build())) + .build(); + + evalService.pushDataset(dto); + + List list = evalService.listDatasets(); + DatasetDto summary = list.stream() + .filter(d -> d.getName().equals(id("dataset-count"))) + .findFirst() + .orElseThrow(); + + assertThat(summary.getCaseCount()).isEqualTo(3); + // List response must NOT include full cases + assertThat(summary.getCases()).isNull(); + } + + @Test + void pushDataset_upserts_onSecondPush() { + DatasetDto first = DatasetDto.builder() + .name(id("dataset-upsert")) + .cases(List.of(DatasetDto.DatasetCaseDto.builder() + .name("c1") + .prompt("p1") + .build())) + .build(); + DatasetDto second = DatasetDto.builder() + .name(id("dataset-upsert")) + .cases(List.of( + DatasetDto.DatasetCaseDto.builder() + .name("c1") + .prompt("p1") + .build(), + DatasetDto.DatasetCaseDto.builder() + .name("c2") + .prompt("p2") + .build())) + .build(); + + evalService.pushDataset(first); + evalService.pushDataset(second); + + DatasetDto retrieved = evalService.getDataset(id("dataset-upsert")); + assertThat(retrieved.getCases()).hasSize(2); + assertThat(retrieved.getCaseCount()).isEqualTo(2); + } + + @Test + void getDataset_throwsNotFound_forMissingName() { + assertThatThrownBy(() -> evalService.getDataset("no-such-dataset")).isInstanceOf(NoSuchElementException.class); + } +} diff --git a/ui/e2e/eval-datasets.spec.ts b/ui/e2e/eval-datasets.spec.ts new file mode 100644 index 000000000..ce398f995 --- /dev/null +++ b/ui/e2e/eval-datasets.spec.ts @@ -0,0 +1,145 @@ +/** + * E2E tests for the Datasets split-panel page. + * + * All backend calls are intercepted — no real server needed. + * No LLM assertions (per CLAUDE.md). + */ +import { expect, Page, test } from "@playwright/test"; + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +const DATASETS_LIST = [ + { + name: "billing-cases", + updatedAt: "2026-05-20T08:00:00Z", + cases: [ + { + name: "refund_request", + prompt: "I need a refund for order #123", + assertions: ["handoff_to:billing", "output_contains:refund"], + tags: ["billing"], + semanticCriteria: "Response acknowledges refund intent", + }, + { + name: "account_query", + prompt: "What is my account balance?", + assertions: ["tool_used:lookup_account"], + tags: [], + semanticCriteria: null, + }, + ], + }, + { + name: "tech-cases", + updatedAt: "2026-05-19T07:00:00Z", + cases: [ + { + name: "crash_report", + prompt: "My app crashes on startup", + assertions: ["handoff_to:technical"], + tags: ["tech"], + semanticCriteria: null, + }, + ], + }, +]; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function mockDatasetApis(page: Page) { + // List all datasets + await page.route("**/api/eval/datasets", async (route) => { + if (route.request().method() === "GET") { + return route.fulfill({ json: DATASETS_LIST }); + } + return route.continue(); + }); + + // Dataset detail by name + await page.route("**/api/eval/datasets/billing-cases", async (route) => { + return route.fulfill({ json: DATASETS_LIST[0] }); + }); + + await page.route("**/api/eval/datasets/tech-cases", async (route) => { + return route.fulfill({ json: DATASETS_LIST[1] }); + }); + + // Silence unrelated API calls + await page.route("**/api/**", async (route) => { + const url = route.request().url(); + if (url.includes("/api/eval/")) return route.fallback(); + return route.fulfill({ json: [] }); + }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test.describe("Datasets split-panel", () => { + test.beforeEach(async ({ page }) => { + await mockDatasetApis(page); + await page.goto("/experiments/datasets"); + }); + + test("page title is 'Datasets'", async ({ page }) => { + await expect(page).toHaveTitle("Datasets"); + }); + + test("left panel lists both datasets", async ({ page }) => { + await expect(page.getByText("billing-cases")).toBeVisible(); + await expect(page.getByText("tech-cases")).toBeVisible(); + }); + + test("right panel shows placeholder when no dataset selected", async ({ page }) => { + await expect(page.getByText("Select a dataset to view its cases")).toBeVisible(); + }); + + test("clicking a dataset shows its cases on the right", async ({ page }) => { + await page.getByText("billing-cases").click(); + // URL updates to include dataset name + await expect(page).toHaveURL(/\/experiments\/datasets\/billing-cases/); + // Right panel shows dataset name + await expect(page.getByText("billing-cases").last()).toBeVisible(); + // Shows cases + await expect(page.getByText("refund_request")).toBeVisible(); + await expect(page.getByText("account_query")).toBeVisible(); + }); + + test("cases table shows Semantic Criterion column", async ({ page }) => { + await page.goto("/experiments/datasets/billing-cases"); + await expect(page.getByText("Semantic Criterion")).toBeVisible(); + await expect(page.getByText("Response acknowledges refund intent")).toBeVisible(); + }); + + test("cases table shows assertions as chips", async ({ page }) => { + await page.goto("/experiments/datasets/billing-cases"); + await expect(page.getByText("handoff_to:billing")).toBeVisible(); + await expect(page.getByText("output_contains:refund")).toBeVisible(); + }); + + test("navigating directly to dataset URL shows split panel with detail", async ({ page }) => { + await page.goto("/experiments/datasets/tech-cases"); + // Left panel still shows both datasets + await expect(page.getByText("billing-cases")).toBeVisible(); + await expect(page.getByText("tech-cases")).toBeVisible(); + // Right panel shows tech-cases detail + await expect(page.getByText("crash_report")).toBeVisible(); + }); + + test("empty state shows push_dataset SDK instruction", async ({ page }) => { + // Mock empty list + await page.route("**/api/eval/datasets", async (route) => { + if (route.request().method() === "GET") { + return route.fulfill({ json: [] }); + } + return route.continue(); + }); + await page.reload(); + await expect(page.getByText(/runtime\.push_dataset/i)).toBeVisible(); + }); +}); diff --git a/ui/e2e/eval-runs.spec.ts b/ui/e2e/eval-runs.spec.ts new file mode 100644 index 000000000..9aba086e5 --- /dev/null +++ b/ui/e2e/eval-runs.spec.ts @@ -0,0 +1,237 @@ +/** + * E2E tests for Eval Runs list and detail pages. + * + * All backend calls are intercepted — no real server needed. + * No LLM assertions (per CLAUDE.md). + */ +import { expect, Page, test } from "@playwright/test"; + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +const EVAL_RUNS_PAGE: Record = { + totalHits: 3, + results: [ + { + id: "run-aaa-111", + name: "eval_handoff_v2", + agentName: "support-agent", + timestamp: "2026-05-20T10:00:00Z", + totalCases: 4, + passedCases: 4, + strategy: "handoff", + }, + { + id: "run-bbb-222", + name: "billing_routing", + agentName: "billing-agent", + timestamp: "2026-05-19T09:00:00Z", + totalCases: 5, + passedCases: 3, + }, + { + id: "run-ccc-333", + agentName: "tech-agent", + timestamp: "2026-05-18T08:00:00Z", + totalCases: 2, + passedCases: 0, + }, + ], +}; + +const EVAL_RUN_DETAIL: Record = { + id: "run-aaa-111", + name: "eval_handoff_v2", + agentName: "support-agent", + timestamp: "2026-05-20T10:00:00Z", + totalCases: 2, + passedCases: 1, + strategy: "handoff", + ranBy: "test_script.py", + cases: [ + { + id: "case-1", + name: "routes_billing_correctly", + passed: true, + prompt: "I need a refund for order #123", + output: "Routing to billing department.", + agentName: "support-agent", + checks: [ + { id: "chk-1", check: "status", passed: true, message: "" }, + { id: "chk-2", check: "handoff_to:billing", passed: true, message: "" }, + ], + }, + { + id: "case-2", + name: "routes_tech_correctly", + passed: false, + prompt: "My app crashes on startup", + output: "Sorry I cannot help.", + agentName: "support-agent", + checks: [ + { id: "chk-3", check: "status", passed: true, message: "" }, + { + id: "chk-4", + check: "handoff_to:technical", + passed: false, + message: "Expected handoff to 'technical', but none occurred.", + }, + { + id: "chk-5", + check: "strategy_validation", + passed: true, + message: "", + score: 0.87, + reasoning: "The agent followed the handoff strategy correctly.", + }, + ], + }, + ], +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function mockEvalApis(page: Page) { + await page.route("**/api/eval/runs?*", async (route) => { + return route.fulfill({ json: EVAL_RUNS_PAGE }); + }); + + await page.route("**/api/eval/runs/run-aaa-111", async (route) => { + return route.fulfill({ json: EVAL_RUN_DETAIL }); + }); + + // Silence unrelated API calls + await page.route("**/api/**", async (route) => { + const url = route.request().url(); + if (url.includes("/api/eval/")) return route.fallback(); + return route.fulfill({ json: [] }); + }); +} + +// --------------------------------------------------------------------------- +// Eval Runs List tests +// --------------------------------------------------------------------------- + +test.describe("Eval Runs list", () => { + test.beforeEach(async ({ page }) => { + await mockEvalApis(page); + await page.goto("/experiments/eval-runs"); + }); + + test("page title is 'Eval Runs'", async ({ page }) => { + await expect(page).toHaveTitle("Eval Runs"); + }); + + test("stats row shows correct totals", async ({ page }) => { + await expect(page.getByText("TOTAL RUNS")).toBeVisible(); + await expect(page.getByText("3")).toBeVisible(); + // 1 run is all-passing (run-aaa-111: 4/4) + await expect(page.getByText("PASSING")).toBeVisible(); + await expect(page.getByText("FAILING")).toBeVisible(); + }); + + test("table shows run names and agent chips", async ({ page }) => { + await expect(page.getByText("eval_handoff_v2")).toBeVisible(); + await expect(page.getByText("billing_routing")).toBeVisible(); + // Third run has no name — should show truncated UUID + await expect(page.getByText("run-ccc-")).toBeVisible(); + }); + + test("table shows agent name chips", async ({ page }) => { + await expect(page.getByText("support-agent")).toBeVisible(); + await expect(page.getByText("billing-agent")).toBeVisible(); + await expect(page.getByText("tech-agent")).toBeVisible(); + }); + + test("cases column shows pass/fail badges", async ({ page }) => { + // billing_routing has 3 pass and 2 fail + await expect(page.getByText("3 pass")).toBeVisible(); + await expect(page.getByText("2 fail")).toBeVisible(); + }); + + test("search filters rows by run name", async ({ page }) => { + const searchInput = page.getByPlaceholder(/search by run name/i); + await searchInput.fill("billing"); + await expect(page.getByText("billing_routing")).toBeVisible(); + await expect(page.getByText("eval_handoff_v2")).not.toBeVisible(); + }); + + test("agent filter dropdown filters by agent", async ({ page }) => { + // Click the agent filter Select + await page.getByRole("combobox").first().click(); + await page.getByRole("option", { name: "support-agent" }).click(); + await expect(page.getByText("eval_handoff_v2")).toBeVisible(); + await expect(page.getByText("billing_routing")).not.toBeVisible(); + }); + + test("clicking a row navigates to detail page", async ({ page }) => { + await page.getByText("eval_handoff_v2").click(); + await expect(page).toHaveURL(/\/experiments\/eval-runs\/run-aaa-111/); + }); +}); + +// --------------------------------------------------------------------------- +// Eval Run Detail tests +// --------------------------------------------------------------------------- + +test.describe("Eval Run Detail", () => { + test.beforeEach(async ({ page }) => { + await mockEvalApis(page); + await page.goto("/experiments/eval-runs/run-aaa-111"); + }); + + test("page title uses run name", async ({ page }) => { + await expect(page).toHaveTitle("eval_handoff_v2"); + }); + + test("section header shows run name", async ({ page }) => { + await expect(page.getByRole("heading", { name: "eval_handoff_v2" })).toBeVisible(); + }); + + test("metadata card shows agent, strategy, ran by", async ({ page }) => { + await expect(page.getByText("support-agent")).toBeVisible(); + await expect(page.getByText("handoff")).toBeVisible(); + await expect(page.getByText("test_script.py")).toBeVisible(); + }); + + test("metadata card shows pass rate bar", async ({ page }) => { + await expect(page.getByText("50%")).toBeVisible(); + }); + + test("case list shows both cases", async ({ page }) => { + await expect(page.getByText("routes_billing_correctly")).toBeVisible(); + await expect(page.getByText("routes_tech_correctly")).toBeVisible(); + }); + + test("case header shows prompt text under case name", async ({ page }) => { + await expect(page.getByText("I need a refund for order #123")).toBeVisible(); + await expect(page.getByText("My app crashes on startup")).toBeVisible(); + }); + + test("expanding a passing case shows checks", async ({ page }) => { + // Click to expand the first case accordion + await page.getByText("routes_billing_correctly").click(); + await expect(page.getByText("handoff_to:billing")).toBeVisible(); + }); + + test("expanding a failing case shows failed check with message", async ({ page }) => { + await page.getByText("routes_tech_correctly").click(); + await expect(page.getByText(/Expected handoff to 'technical'/i)).toBeVisible(); + }); + + test("expanding a case shows agent output box", async ({ page }) => { + await page.getByText("routes_billing_correctly").click(); + await expect(page.getByText("AGENT OUTPUT")).toBeVisible(); + await expect(page.getByText("Routing to billing department.")).toBeVisible(); + }); + + test("semantic check shows score and reasoning in purple box", async ({ page }) => { + await page.getByText("routes_tech_correctly").click(); + // Score from fixture is 0.87 + await expect(page.getByText("0.87")).toBeVisible(); + await expect(page.getByText("The agent followed the handoff strategy correctly.")).toBeVisible(); + }); +}); diff --git a/ui/src/components/Sidebar/sidebarCoreItems.tsx b/ui/src/components/Sidebar/sidebarCoreItems.tsx index 5bf1fc7dc..ac61171b2 100644 --- a/ui/src/components/Sidebar/sidebarCoreItems.tsx +++ b/ui/src/components/Sidebar/sidebarCoreItems.tsx @@ -13,6 +13,7 @@ import CodeIcon from "@mui/icons-material/Code"; import MenuBookOutlinedIcon from "@mui/icons-material/MenuBookOutlined"; import PlayIcon from "@mui/icons-material/PlayArrowOutlined"; import PlaylistPlayIcon from "@mui/icons-material/PlaylistPlay"; +import ScienceOutlinedIcon from "@mui/icons-material/ScienceOutlined"; import WebhookOutlinedIcon from "@mui/icons-material/WebhookOutlined"; import RunAgentButton from "components/Sidebar/RunAgentButton"; import DiscordIcon from "components/Sidebar/DiscordIcon"; @@ -20,6 +21,7 @@ import { MenuItemType } from "components/Sidebar/types"; import { CREDENTIALS_URL, + EXPERIMENTS_URL, RUN_AGENT_URL, TASK_QUEUE_URL, AGENT_DEFINITION_URL, @@ -37,6 +39,7 @@ const CORE_SIDEBAR_POSITIONS = { executionsSubMenu: 100, runWorkflow: 200, definitionsSubMenu: 300, + experimentsSubMenu: 400, swaggerItem: 500, docsItem: 600, discordItem: 700, @@ -51,6 +54,11 @@ const CORE_SIDEBAR_POSITIONS = { workflowDefItem: 100, credentialsItem: 200, }, + // Experiments submenu children + EXPERIMENTS: { + evalRunsItem: 100, + datasetsItem: 200, + }, } as const; /** @@ -62,6 +70,7 @@ export function getCoreSidebarItems(open: boolean): MenuItemType[] { const R = CORE_SIDEBAR_POSITIONS.ROOT; const E = CORE_SIDEBAR_POSITIONS.EXECUTIONS; const D = CORE_SIDEBAR_POSITIONS.DEFINITIONS; + const X = CORE_SIDEBAR_POSITIONS.EXPERIMENTS; return [ // Executions submenu - core items only @@ -146,6 +155,41 @@ export function getCoreSidebarItems(open: boolean): MenuItemType[] { }, ], }, + // Experiments submenu + { + id: "experimentsSubMenu", + title: "Experiments", + icon: , + linkTo: "", + shortcuts: [], + hotkeys: "", + hidden: false, + position: R.experimentsSubMenu, + items: [ + { + id: "evalRunsItem", + title: "Eval Runs", + icon: null, + linkTo: EXPERIMENTS_URL.EVAL_RUNS, + activeRoutes: [EXPERIMENTS_URL.EVAL_RUN_DETAIL], + shortcuts: [], + hotkeys: "", + hidden: false, + position: X.evalRunsItem, + }, + { + id: "datasetsItem", + title: "Datasets", + icon: null, + linkTo: EXPERIMENTS_URL.DATASETS, + activeRoutes: [EXPERIMENTS_URL.DATASET_DETAIL], + shortcuts: [], + hotkeys: "", + hidden: false, + position: X.datasetsItem, + }, + ], + }, // API Docs { id: "swaggerItem", diff --git a/ui/src/pages/executions/AgentSearch.tsx b/ui/src/pages/executions/AgentSearch.tsx index 04c3dbf26..232e8b275 100644 --- a/ui/src/pages/executions/AgentSearch.tsx +++ b/ui/src/pages/executions/AgentSearch.tsx @@ -50,6 +50,7 @@ const SwitchComponent = ({ export default function AgentPanel() { const [asQuery, setAsQuery] = useQueryState("asQuery", false); + const [includeEvalRuns, setIncludeEvalRuns] = useQueryState("includeEvalRuns", false); const [freeText, setFreeText] = useQueryState("freeText", ""); const [status, setStatus] = useQueryState("status", []); const [openDateSelect, setOpenDateSelect] = useState(false); @@ -191,6 +192,8 @@ export default function AgentPanel() { openEndDatePicker={openEndDatePicker} setEndOpenDatePicker={setEndOpenDatePicker} recentSearches={recentSearches} + includeEvalRuns={includeEvalRuns} + onToggleEvalRuns={() => setIncludeEvalRuns(!includeEvalRuns)} /> ) : ( setIncludeEvalRuns(!includeEvalRuns)} /> )} diff --git a/ui/src/pages/executions/ResultsTable.tsx b/ui/src/pages/executions/ResultsTable.tsx index 231bf6c4d..e7c8773e2 100644 --- a/ui/src/pages/executions/ResultsTable.tsx +++ b/ui/src/pages/executions/ResultsTable.tsx @@ -2,6 +2,7 @@ import { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { DataTable, NavLink, Paper, Text } from "components"; import { Box, + Chip, FormControlLabel, LinearProgress, Switch, @@ -9,6 +10,7 @@ import { TableBody, TableCell, TableRow, + Typography, } from "@mui/material"; import BulkActionModule from "./BulkActionModule"; import executionsStyles from "./executionsStyles"; @@ -18,6 +20,19 @@ import { SnackbarMessage } from "components/SnackbarMessage"; import { ColumnCustomType, LegacyColumn } from "components/DataTable/types"; import NoDataComponent from "components/NoDataComponent"; import { colors } from "theme/tokens/variables"; +import { EXPERIMENTS_URL } from "utils/constants/route"; + +const isEvalRow = (row: any): boolean => { + if (typeof row.input !== "string") return false; + // Handle both JSON format ({"session_id":"eval:..."}) and + // Map.toString() format ({session_id=eval:...}) from Conductor WorkflowSummary + return ( + row.input.includes('"session_id":"eval:') || + row.input.includes("session_id=eval:") || + row.input.includes('"_eval_run":true') || + row.input.includes("_eval_run=true") + ); +}; const LinearIndeterminate = () => { return ( @@ -28,6 +43,19 @@ const LinearIndeterminate = () => { }; const executionFields: LegacyColumn[] = [ + { + id: "type", + name: "workflowId", + label: "Type", + grow: 0.6, + sortable: false, + renderer: (_val: any, row: any) => + isEvalRow(row) ? ( + + ) : ( + + ), + }, { id: "startTime", name: "startTime", @@ -219,6 +247,8 @@ export interface ResultsTableProps { handleClearError?: () => void; filterOn: boolean; handleReset: () => void; + includeEvalRuns?: boolean; + onToggleEvalRuns?: () => void; } export default function ResultsTable({ @@ -236,6 +266,8 @@ export default function ResultsTable({ handleClearError, filterOn, handleReset, + includeEvalRuns = false, + onToggleEvalRuns, }: ResultsTableProps) { const [selectedRows, setSelectedRows] = useState([]); const [toggleCleared, setToggleCleared] = useState(false); @@ -324,17 +356,30 @@ export default function ResultsTable({ )} {resultObj?.results && ( - setHideSubWorkflows(e.target.checked)} - size="small" + + setHideSubWorkflows(e.target.checked)} + size="small" + /> + } + label="Hide sub-agent executions" + /> + {onToggleEvalRuns !== undefined && ( + + } + label="Show eval runs" /> - } - label="Hide sub-agent executions" - sx={{ ml: 1, mb: 1 }} - /> + )} + )} row._isSubAgent, style: { backgroundColor: "#f9f9f9", opacity: 0.8 }, }, + { + when: (row: any) => includeEvalRuns && isEvalRow(row), + style: { backgroundColor: "#f9f9f9", opacity: 0.8 }, + }, ]} customStyles={{ header: { @@ -471,6 +521,29 @@ export default function ResultsTable({ ) } /> + {resultObj?.results && (() => { + const all: any[] = resultObj.results; + const evalCount = all.filter(isEvalRow).length; + const prodCount = all.length - evalCount; + if (includeEvalRuns && evalCount > 0) { + return ( + + {prodCount} production · {evalCount} eval (greyed) + + ); + } + if (!includeEvalRuns) { + return ( + + {all.length} production runs · eval runs hidden —{" "} + + view in Experiments + + + ); + } + return null; + })()} ); } diff --git a/ui/src/pages/executions/workflowSearchComponents/AdvancedSearch.tsx b/ui/src/pages/executions/workflowSearchComponents/AdvancedSearch.tsx index 7d8cd0cdf..6cb51a0d9 100644 --- a/ui/src/pages/executions/workflowSearchComponents/AdvancedSearch.tsx +++ b/ui/src/pages/executions/workflowSearchComponents/AdvancedSearch.tsx @@ -81,6 +81,8 @@ export interface AdvancedSearchProps { openEndDatePicker: boolean; setEndOpenDatePicker: (val: boolean) => void; recentSearches: { start: string; end: string }; + includeEvalRuns: boolean; + onToggleEvalRuns?: () => void; } export default function AdvancedSearch({ @@ -114,6 +116,8 @@ export default function AdvancedSearch({ openEndDatePicker, setEndOpenDatePicker, recentSearches, + includeEvalRuns, + onToggleEvalRuns, }: AdvancedSearchProps) { const disposeRef = useRef void)>(null); const [queryText, setQueryText] = useQueryState("query", ""); @@ -208,6 +212,7 @@ export default function AdvancedSearch({ sort, query: queryFT.query, freeText: queryFT.freeText, + includeEvalRuns, }, {}, { @@ -583,6 +588,8 @@ export default function AdvancedSearch({ filterOn={filterOn} handleReset={handleReset} setRowsPerPage={handleRowsPerPage} + includeEvalRuns={includeEvalRuns} + onToggleEvalRuns={onToggleEvalRuns} /> ); diff --git a/ui/src/pages/executions/workflowSearchComponents/BasicSearch.tsx b/ui/src/pages/executions/workflowSearchComponents/BasicSearch.tsx index 9f6e0ef9e..474b6978d 100644 --- a/ui/src/pages/executions/workflowSearchComponents/BasicSearch.tsx +++ b/ui/src/pages/executions/workflowSearchComponents/BasicSearch.tsx @@ -76,6 +76,8 @@ export interface BasicSearchProps { openEndDatePicker: boolean; setEndOpenDatePicker: (val: boolean) => void; recentSearches: { start: string; end: string }; + includeEvalRuns: boolean; + onToggleEvalRuns?: () => void; } export default function BasicSearch({ @@ -109,6 +111,8 @@ export default function BasicSearch({ openEndDatePicker, setEndOpenDatePicker, recentSearches, + includeEvalRuns, + onToggleEvalRuns, }: BasicSearchProps) { const [page, setPage] = useQueryState("page", 1); const [workflowType, setWorkflowType] = useQueryState( @@ -278,6 +282,7 @@ export default function BasicSearch({ sort, query: queryFT.query, freeText: queryFT.freeText, + includeEvalRuns, }, {}, { @@ -694,6 +699,8 @@ export default function BasicSearch({ filterOn={filterOn} handleReset={handleReset} setRowsPerPage={handleRowsPerPage} + includeEvalRuns={includeEvalRuns} + onToggleEvalRuns={onToggleEvalRuns} /> ); diff --git a/ui/src/pages/experiments/DatasetsList.tsx b/ui/src/pages/experiments/DatasetsList.tsx new file mode 100644 index 000000000..1337f861e --- /dev/null +++ b/ui/src/pages/experiments/DatasetsList.tsx @@ -0,0 +1,231 @@ +import { + Box, + Chip, + Divider, + LinearProgress, + List, + ListItemButton, + ListItemText, + Typography, +} from "@mui/material"; +import { DataTable } from "components"; +import { LegacyColumn } from "components/DataTable/types"; // used by caseColumns +import { Helmet } from "react-helmet"; +import { useNavigate, useParams } from "react-router"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import { EXPERIMENTS_URL } from "utils/constants/route"; +import { type Dataset, type DatasetCase, useDataset, useDatasets } from "./useEvalApi"; + + +const caseColumns: LegacyColumn[] = [ + { id: "name", name: "name", label: "Case Name" }, + { + id: "prompt", + name: "prompt", + label: "Prompt", + renderer: (val: string) => ( + + {val} + + ), + }, + { + id: "semanticCriteria", + name: "semanticCriteria", + label: "Semantic Criterion", + renderer: (val: string) => + val ? ( + + {val} + + ) : ( + + — + + ), + }, + { + id: "assertions", + name: "assertions", + label: "Assertions", + renderer: (val: string[]) => + val?.length ? ( + + {val.map((a, i) => ( + + ))} + + ) : ( + "—" + ), + }, + { + id: "tags", + name: "tags", + label: "Tags", + renderer: (val: string[]) => + val?.length ? ( + + {val.map((t, i) => ( + + ))} + + ) : ( + "—" + ), + }, +]; + +function DatasetDetailPanel({ name }: { name: string }) { + const { data: dataset, isLoading } = useDataset(decodeURIComponent(name)); + const rows: DatasetCase[] = dataset?.cases ?? []; + + return ( + + {isLoading && } + {dataset && ( + <> + + + {dataset.name} + + + {dataset.updatedAt && ( + + Last updated: {new Date(dataset.updatedAt).toLocaleString()} + + )} + {dataset.pushedBy && ( + + Pushed by: {dataset.pushedBy} + + )} + + + {rows.length === 0 ? ( + No cases in this dataset. + ) : ( + + )} + + )} + + ); +} + +export default function DatasetsList() { + const navigate = useNavigate(); + const { name: selectedName } = useParams<{ name?: string }>(); + const { data: datasets, isLoading } = useDatasets(); + + const rows: Dataset[] = datasets ?? []; + + return ( + <> + + Datasets + + + + {isLoading && !rows.length ? ( + + ) : !rows.length ? ( + + No datasets yet. Push a dataset with{" "} + runtime.push_dataset(name, cases) from the Python SDK. + + ) : ( + + {/* Left sidebar — dataset list */} + + + {rows.length} dataset{rows.length !== 1 ? "s" : ""} + + + {rows.map((row) => { + const isSelected = + !!selectedName && decodeURIComponent(selectedName) === row.name; + return ( + { + if (row.name) + navigate( + EXPERIMENTS_URL.DATASETS + "/" + encodeURIComponent(row.name), + ); + }} + sx={{ + borderLeft: "3px solid", + borderColor: isSelected ? "secondary.main" : "transparent", + "&.Mui-selected": { bgcolor: "rgba(91,106,240,0.07)" }, + }} + > + + {row.name} + + } + secondary={ + + {row.caseCount ?? 0} cases + {row.updatedAt + ? ` · ${new Date(row.updatedAt).toLocaleDateString()}` + : ""} + + } + /> + + ); + })} + + + {selectedName && ( + <> + + + + )} + {!selectedName && ( + + Select a dataset to view its cases + + )} + + )} + + + ); +} diff --git a/ui/src/pages/experiments/EvalRunDetail.tsx b/ui/src/pages/experiments/EvalRunDetail.tsx new file mode 100644 index 000000000..2482cd019 --- /dev/null +++ b/ui/src/pages/experiments/EvalRunDetail.tsx @@ -0,0 +1,375 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Chip, + LinearProgress, + Typography, +} from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; +import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined"; +import { Helmet } from "react-helmet"; +import { useNavigate, useParams } from "react-router"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import { EXPERIMENTS_URL } from "utils/constants/route"; +import { type EvalCase, type EvalCheck, type EvalRun, useEvalRun } from "./useEvalApi"; + +function PassFailBadge({ passed }: { passed: boolean }) { + return ( + + {passed ? "PASS" : "FAIL"} + + ); +} + +function SemanticBox({ check }: { check: EvalCheck }) { + const passed = check.passed; + return ( + + + ⬡ Semantic Score — LLM Judge{!passed ? " (FAIL)" : ""} + + + {check.score?.toFixed(2)} + + {check.reasoning && ( + + {check.reasoning} + + )} + + ); +} + +function CheckRow({ check }: { check: EvalCheck }) { + const icon = check.passed ? ( + + ) : ( + + ); + + if (check.score != null) { + return ( + + + {icon} + + {check.check} + + + + + ); + } + + return ( + + {icon} + + + {check.check} + + {!check.passed && check.message && ( + + {check.message} + + )} + + + ); +} + +function CaseAccordion({ evalCase }: { evalCase: EvalCase }) { + const checks = evalCase.checks ?? []; + const totalChecks = checks.length; + const passedChecks = checks.filter((c) => c.passed).length; + const failedChecks = totalChecks - passedChecks; + const highestScore = checks.reduce((max, c) => { + if (c.score == null) return max; + return max == null || c.score > max ? c.score : max; + }, null); + + const checkSummary = totalChecks > 0 + ? `${passedChecks}/${totalChecks} checks${failedChecks > 0 ? ` · ${failedChecks} failed` : ""}${highestScore != null ? ` · semantic ${highestScore.toFixed(2)}` : ""}` + : ""; + + return ( + + } + sx={{ px: 2, py: 1.25, minHeight: "unset", "& .MuiAccordionSummary-content": { my: 0 } }} + > + + + + + {evalCase.name} + + {evalCase.prompt && ( + + "{evalCase.prompt}" + + )} + + + + {checkSummary && ( + 0 ? "error.main" : "text.secondary"} + fontWeight={failedChecks > 0 ? 600 : 400} + > + {checkSummary} + + )} + {evalCase.agentName && ( + + )} + + + + {evalCase.error && ( + + Error: {evalCase.error} + + )} + {checks.map((check, i) => ( + + ))} + {evalCase.output && ( + + + Agent Output + + + {evalCase.output} + + + )} + + + ); +} + +function strategyValidSummary(run: EvalRun): string | null { + if (!run.strategy || !run.cases?.length) return null; + const total = run.cases.length; + const validCount = run.cases.filter((c) => + c.checks?.some((ch) => ch.check === "strategy_validation" && ch.passed), + ).length; + if (validCount === total) return `✓ all ${validCount}`; + return `${validCount}/${total} valid`; +} + +export default function EvalRunDetail() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { data: run, isLoading } = useEvalRun(id ?? ""); + + const pct = + run && run.totalCases > 0 + ? Math.round((run.passedCases / run.totalCases) * 100) + : 0; + + const displayName = run?.name || run?.agentName || "Eval Run"; + const strategyValid = run ? strategyValidSummary(run) : null; + + return ( + <> + + {run?.name ?? (run ? `Eval Run — ${run.agentName}` : "Eval Run")} + + + + {isLoading && } + {run && ( + <> + {/* Back link */} + navigate(EXPERIMENTS_URL.EVAL_RUNS)} + sx={{ + color: "secondary.main", + cursor: "pointer", + display: "inline-flex", + alignItems: "center", + gap: 0.5, + mb: 2, + "&:hover": { textDecoration: "underline" }, + }} + > + ← Back to Eval Runs + + + {/* Run name heading */} + + {displayName} + + + {/* Compact metadata card — two rows */} + + {/* Row 1 */} + + + Agent: + + + {run.strategy && ( + + Strategy: + {run.strategy} + + )} + + Cases: + {run.totalCases} + + + Passed: + {run.passedCases} + + + Failed: + 0 ? "error.main" : "text.secondary"}> + {run.totalCases - run.passedCases} + + + {/* Pass rate bar inline */} + + = 50 ? "warning" : "error"} + sx={{ width: 80, height: 6, borderRadius: 3 }} + /> + {pct}% + + + + {/* Row 2 — optional fields */} + {(strategyValid || run.ranBy || run.createdBy || run.timestamp) && ( + + {strategyValid && ( + + Strategy valid: + {strategyValid} + + )} + {run.ranBy && ( + + Ran by: + {run.ranBy} + + )} + {run.createdBy && ( + + Created by: + {run.createdBy} + + )} + {run.timestamp && ( + + {new Date(run.timestamp).toLocaleString()} + + )} + + )} + + + {/* Cases section */} + + + {run.cases?.length ?? 0} Cases + + + {(run.cases ?? []).map((c, i) => ( + + ))} + + {!run.cases?.length && ( + + No cases in this run. + + )} + + + )} + + + ); +} diff --git a/ui/src/pages/experiments/EvalRunsList.tsx b/ui/src/pages/experiments/EvalRunsList.tsx new file mode 100644 index 000000000..af3b9eba0 --- /dev/null +++ b/ui/src/pages/experiments/EvalRunsList.tsx @@ -0,0 +1,279 @@ +import { Box, Card, CardContent, Chip, LinearProgress, TextField, Typography } from "@mui/material"; +import MenuItem from "@mui/material/MenuItem"; +import Select from "@mui/material/Select"; +import { DataTable } from "components"; +import { useMemo, useState } from "react"; +import { Helmet } from "react-helmet"; +import { useNavigate } from "react-router"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import { LegacyColumn } from "components/DataTable/types"; +import { EXPERIMENTS_URL } from "utils/constants/route"; +import { useEvalRuns, type EvalRun } from "./useEvalApi"; + +function PassRateBar({ run }: { run: EvalRun }) { + const pct = + run.totalCases > 0 ? Math.round((run.passedCases / run.totalCases) * 100) : 0; + const color = pct === 100 ? "success" : pct >= 50 ? "warning" : "error"; + return ( + + + + {run.passedCases}/{run.totalCases} + + + ); +} + +function CaseBadges({ run }: { run: EvalRun }) { + const failed = run.totalCases - run.passedCases; + return ( + + {run.passedCases > 0 && ( + + )} + {failed > 0 && ( + + )} + + ); +} + +function StatCard({ + label, + value, + color, +}: { + label: string; + value: number | string; + color?: string; +}) { + return ( + + + + {label} + + + {value} + + + + ); +} + +const TAG = "allowRowEvents"; + +const columns: LegacyColumn[] = [ + { + id: "name", + name: "name", + label: "Run Name", + renderer: (val: string, row: EvalRun) => ( + + {val || row.id?.slice(0, 8)} + + ), + }, + { + id: "agentName", + name: "agentName", + label: "Agent", + renderer: (val: string) => + val ? ( + + + + ) : ( + + ), + }, + { + id: "passRate", + name: "passedCases", + label: "Pass Rate", + renderer: (_val: number, row: EvalRun) => ( + + + + ), + }, + { + id: "cases", + name: "totalCases", + label: "Cases", + renderer: (_val: number, row: EvalRun) => ( + + + + ), + }, + { + id: "strategyValid", + name: "strategy", + label: "Strategy Valid", + renderer: (val: string) => + val ? ( + + + + ) : ( + + — + + ), + }, + { + id: "timestamp", + name: "timestamp", + label: "Ran At", + renderer: (val: string) => ( + + {val ? new Date(val).toLocaleString() : "—"} + + ), + }, +]; + +export default function EvalRunsList() { + const navigate = useNavigate(); + const { data, isLoading } = useEvalRuns(0, 200); + const [search, setSearch] = useState(""); + const [agentFilter, setAgentFilter] = useState("all"); + const [resultFilter, setResultFilter] = useState("all"); + + const allRows: EvalRun[] = data?.results ?? []; + + const agents = useMemo( + () => Array.from(new Set(allRows.map((r) => r.agentName).filter(Boolean))), + [allRows], + ); + + const rows = useMemo(() => { + return allRows.filter((r) => { + const q = search.toLowerCase(); + const matchSearch = + !q || + (r.name ?? "").toLowerCase().includes(q) || + (r.agentName ?? "").toLowerCase().includes(q); + const matchAgent = agentFilter === "all" || r.agentName === agentFilter; + const pct = r.totalCases > 0 ? r.passedCases / r.totalCases : 0; + const matchResult = + resultFilter === "all" || + (resultFilter === "passing" && pct === 1) || + (resultFilter === "failing" && pct < 1); + return matchSearch && matchAgent && matchResult; + }); + }, [allRows, search, agentFilter, resultFilter]); + + const passing = allRows.filter( + (r) => r.passedCases === r.totalCases && r.totalCases > 0, + ).length; + const failing = allRows.length - passing; + const avgPct = + allRows.length > 0 + ? Math.round( + (allRows.reduce( + (s, r) => s + (r.totalCases > 0 ? r.passedCases / r.totalCases : 0), + 0, + ) / + allRows.length) * + 100, + ) + : 0; + + return ( + <> + + Eval Runs + + + + {isLoading && } + {!isLoading && ( + <> + {/* Stats row */} + + + + + + + + {/* Filters */} + + setSearch(e.target.value)} + sx={{ width: 280 }} + /> + + + + + {allRows.length >= 200 && ( + + Showing the 200 most recent runs. Older runs may not be visible. + + )} + {rows.length === 0 ? ( + + {allRows.length === 0 + ? "No eval runs yet. Run an eval suite with the Python SDK to see results here." + : "No runs match the current filters."} + + ) : ( + { + if (row.id) navigate(EXPERIMENTS_URL.EVAL_RUNS + "/" + row.id); + }} + pointerOnHover + highlightOnHover + paginationPerPage={20} + paginationRowsPerPageOptions={[10, 20, 50]} + /> + )} + + )} + + + ); +} diff --git a/ui/src/pages/experiments/index.ts b/ui/src/pages/experiments/index.ts new file mode 100644 index 000000000..9f2c28237 --- /dev/null +++ b/ui/src/pages/experiments/index.ts @@ -0,0 +1,3 @@ +export { default as EvalRunsList } from "./EvalRunsList"; +export { default as EvalRunDetail } from "./EvalRunDetail"; +export { default as DatasetsList } from "./DatasetsList"; diff --git a/ui/src/pages/experiments/useEvalApi.ts b/ui/src/pages/experiments/useEvalApi.ts new file mode 100644 index 000000000..a1228dfbc --- /dev/null +++ b/ui/src/pages/experiments/useEvalApi.ts @@ -0,0 +1,107 @@ +import { fetchWithContext, useFetchContext } from "plugins/fetch"; +import { useQuery, UseQueryResult } from "react-query"; +import { useAuthHeaders } from "utils/query"; + +export interface EvalCheck { + id: string; + check: string; + passed: boolean; + message?: string; + score?: number; + reasoning?: string; +} + +export interface EvalCase { + id: string; + name: string; + passed: boolean; + error?: string; + agentName?: string; + model?: string; + tags?: string[]; + prompt?: string; + output?: string; + checks?: EvalCheck[]; +} + +export interface EvalRun { + id: string; + agentName: string; + timestamp: string; + totalCases: number; + passedCases: number; + tags?: string[]; + createdBy?: string; + name?: string; + strategy?: string; + ranBy?: string; + cases?: EvalCase[]; +} + +export interface EvalRunsPage { + totalHits: number; + results: EvalRun[]; +} + +export interface DatasetCase { + name: string; + prompt: string; + assertions?: string[]; + tags?: string[]; + semanticCriteria?: string; +} + +export interface Dataset { + name: string; + updatedAt?: string; + pushedBy?: string; + caseCount?: number; + cases?: DatasetCase[]; +} + +export function useEvalRuns( + start = 0, + size = 20, +): UseQueryResult { + const fetchContext = useFetchContext(); + const headers = useAuthHeaders(); + return useQuery( + [fetchContext.stack, "eval/runs", start, size], + () => + fetchWithContext(`eval/runs?start=${start}&size=${size}`, fetchContext, { + headers, + }), + { keepPreviousData: true }, + ); +} + +export function useEvalRun(id: string): UseQueryResult { + const fetchContext = useFetchContext(); + const headers = useAuthHeaders(); + return useQuery( + [fetchContext.stack, "eval/runs", id], + () => fetchWithContext(`eval/runs/${id}`, fetchContext, { headers }), + { enabled: !!id }, + ); +} + +export function useDatasets(): UseQueryResult { + const fetchContext = useFetchContext(); + const headers = useAuthHeaders(); + return useQuery([fetchContext.stack, "eval/datasets"], () => + fetchWithContext("eval/datasets", fetchContext, { headers }), + ); +} + +export function useDataset(name: string): UseQueryResult { + const fetchContext = useFetchContext(); + const headers = useAuthHeaders(); + return useQuery( + [fetchContext.stack, "eval/datasets", name], + () => + fetchWithContext(`eval/datasets/${encodeURIComponent(name)}`, fetchContext, { + headers, + }), + { enabled: !!name }, + ); +} diff --git a/ui/src/routes/routes.tsx b/ui/src/routes/routes.tsx index 79ad497de..a090a230a 100644 --- a/ui/src/routes/routes.tsx +++ b/ui/src/routes/routes.tsx @@ -36,12 +36,18 @@ import { Agent as AgentDefinitions, } from "pages/definitions"; import ErrorPage from "pages/error/ErrorPage"; +import { + DatasetsList, + EvalRunDetail, + EvalRunsList, +} from "pages/experiments"; import { SchedulerExecutions, AgentSearch } from "pages/executions"; import { pluginRegistry } from "plugins/registry"; import { featureFlags, FEATURES } from "utils"; import { API_REFERENCE_URL, CREDENTIALS_URL, + EXPERIMENTS_URL, RUN_AGENT_URL, SCHEDULER_DEFINITION_URL, TASK_QUEUE_URL, @@ -112,6 +118,26 @@ const getCoreAuthenticatedRoutes = () => [ element: , }, + // Experiments — Eval Runs + { + path: EXPERIMENTS_URL.EVAL_RUNS, + element: , + }, + { + path: EXPERIMENTS_URL.EVAL_RUN_DETAIL, + element: , + }, + + // Experiments — Datasets (split-panel; DATASET_DETAIL also renders DatasetsList with selection) + { + path: EXPERIMENTS_URL.DATASETS, + element: , + }, + { + path: EXPERIMENTS_URL.DATASET_DETAIL, + element: , + }, + ]; /** diff --git a/ui/src/utils/constants/route.ts b/ui/src/utils/constants/route.ts index 7890b9dd1..b499723b9 100644 --- a/ui/src/utils/constants/route.ts +++ b/ui/src/utils/constants/route.ts @@ -146,3 +146,11 @@ export const API_REFERENCE_URL = { }; export const CREDENTIALS_URL = "/credentials"; + +export const EXPERIMENTS_URL = { + BASE: "/experiments", + EVAL_RUNS: "/experiments/eval-runs", + EVAL_RUN_DETAIL: "/experiments/eval-runs/:id", + DATASETS: "/experiments/datasets", + DATASET_DETAIL: "/experiments/datasets/:name", +}; diff --git a/ui/src/utils/query.ts b/ui/src/utils/query.ts index 9bb6bb9e0..7d64b2f0d 100644 --- a/ui/src/utils/query.ts +++ b/ui/src/utils/query.ts @@ -51,6 +51,7 @@ export interface SearchObj { freeText?: string; query?: string; queryId?: string; + includeEvalRuns?: boolean; } export interface TaskSearchObj extends Omit { @@ -214,7 +215,7 @@ export function useSearch( return useQuery( [fetchContext.stack, pathRoot, searchObj], () => { - const { rowsPerPage, page, sort, freeText, query } = searchObj; + const { rowsPerPage, page, sort, freeText, query, includeEvalRuns } = searchObj; let params: IObject = { start: (page - 1) * rowsPerPage, size: rowsPerPage, @@ -222,6 +223,9 @@ export function useSearch( freeText: freeText, query: query, }; + if (includeEvalRuns) { + params = { ...params, includeEvalRuns: true }; + } if (searchObj.queryId) { params = { queryId: searchObj.queryId, ...params }; } From b31a9d8e876ef1798583aef79ebefebaaca984b6 Mon Sep 17 00:00:00 2001 From: vishesh-orkes Date: Thu, 21 May 2026 11:47:02 +0530 Subject: [PATCH 02/61] fix(eval): use short-lived AsyncClient for push_dataset/post_eval_run to avoid event-loop-closed error --- .../agentspan/agents/runtime/http_client.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/sdk/python/src/agentspan/agents/runtime/http_client.py b/sdk/python/src/agentspan/agents/runtime/http_client.py index cc0e135b4..a3736e39b 100644 --- a/sdk/python/src/agentspan/agents/runtime/http_client.py +++ b/sdk/python/src/agentspan/agents/runtime/http_client.py @@ -263,27 +263,33 @@ def _eval_url(self, path: str) -> str: async def post_eval_run(self, payload: Dict[str, Any]) -> Dict[str, Any]: """POST /eval/runs — submit an eval suite result.""" - client = await self._get_client() url = self._eval_url("/runs") - resp = await client.post(url, json=payload) - try: - resp.raise_for_status() - except httpx.HTTPStatusError as exc: - _raise_api_error(exc, url=url) - return resp.json() if resp.content else {} + async with httpx.AsyncClient( + timeout=httpx.Timeout(30.0, connect=5.0), + headers=self._base_headers(), + ) as client: + resp = await client.post(url, json=payload) + try: + resp.raise_for_status() + except httpx.HTTPStatusError as exc: + _raise_api_error(exc, url=url) + return resp.json() if resp.content else {} async def push_dataset(self, name: str, cases: List[Dict[str, Any]], *, pushed_by: Optional[str] = None) -> None: """POST /eval/datasets — upsert a dataset by name.""" - client = await self._get_client() url = self._eval_url("/datasets") payload: Dict[str, Any] = {"name": name, "cases": cases} if pushed_by is not None: payload["pushedBy"] = pushed_by - resp = await client.post(url, json=payload) - try: - resp.raise_for_status() - except httpx.HTTPStatusError as exc: - _raise_api_error(exc, url=url) + async with httpx.AsyncClient( + timeout=httpx.Timeout(30.0, connect=5.0), + headers=self._base_headers(), + ) as client: + resp = await client.post(url, json=payload) + try: + resp.raise_for_status() + except httpx.HTTPStatusError as exc: + _raise_api_error(exc, url=url) # ── Lifecycle ──────────────────────────────────────────────────── From 6b82b05b3a2ade28e7a65f8026b3bd511f82efd1 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Mon, 8 Jun 2026 14:30:33 -0700 Subject: [PATCH 03/61] =?UTF-8?q?feat(server):=20OCG=20sub-agent=20?= =?UTF-8?q?=E2=80=94=20system=20tasks=20+=20agent-driven=20auto-injection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an optional OCG (Open Context Graph) retrieval sub-agent that the main agent's LLM can delegate to when it decides it needs context. Whole feature is gated on agentspan.ocg.url; unset means every OCG bean stays out of the context and no behavior changes. - 7 OCG_* WorkflowSystemTask beans (OcgRequestTask) — one per OCG endpoint (query, get_entity, neighborhood, code_history, memory_set/reinforce/delete). Each proxies a single HTTP call with field projection + response capping (response-cap-chars, default 8192) so a large graph traversal can't blow the model's context. - _ocg_agent workflow registered at startup by OcgSubAgentService — a normal AgentConfig built by OcgAgentFactory with the OCG system prompt and the seven ocg_* tools. - OcgAgentToolInjector silently appends an ocg_agent agent_tool to every top-level AgentConfig at compile time (skips self-injection on _ocg_agent and duplicate injection if a user already declared it). Main agent's LLM sees it as a peer it can call; tool call dispatches SUB_WORKFLOW(_ocg_agent) which runs its own LLM ↔ ocg_* tool loop and returns a synthesized answer. - ToolCompiler TYPE_MAP + enrichment script (both static and dynamic variants) get an OCG bucket so ocg_* tools route to the right OCG_* task type at runtime. --- .../runtime/compiler/ToolCompiler.java | 59 ++- .../runtime/ocg/OcgAgentFactory.java | 243 +++++++++++++ .../runtime/ocg/OcgAgentToolInjector.java | 78 ++++ .../agentspan/runtime/ocg/OcgProperties.java | 42 +++ .../agentspan/runtime/ocg/OcgRequestTask.java | 340 ++++++++++++++++++ .../runtime/ocg/OcgRequestTaskConfig.java | 65 ++++ .../runtime/ocg/OcgSubAgentService.java | 69 ++++ .../runtime/service/AgentService.java | 43 ++- .../runtime/util/JavaScriptBuilder.java | 26 +- .../src/main/resources/application.properties | 12 + .../runtime/ocg/OcgAgentFactoryTest.java | 83 +++++ .../runtime/ocg/OcgAgentToolInjectorTest.java | 112 ++++++ .../runtime/ocg/OcgRequestTaskTest.java | 187 ++++++++++ .../ocg/OcgToolCompilerIntegrationTest.java | 52 +++ .../runtime/util/EnrichToolsScriptTest.java | 4 +- 15 files changed, 1402 insertions(+), 13 deletions(-) create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentToolInjector.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/OcgSubAgentService.java create mode 100644 server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java create mode 100644 server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentToolInjectorTest.java create mode 100644 server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java create mode 100644 server/src/test/java/dev/agentspan/runtime/ocg/OcgToolCompilerIntegrationTest.java diff --git a/server/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java b/server/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java index 5a7eea1b7..d7066455a 100644 --- a/server/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java +++ b/server/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -85,6 +86,21 @@ private static Map escapeHeadersInConfig(Map cfg /** RAG tool types that map to Conductor RAG system tasks. */ private static final Set RAG_TOOL_TYPES = Set.of("rag_index", "rag_search"); + /** + * OCG tool types — each maps to a {@code OCG_*} {@code WorkflowSystemTask} + * registered by {@code OcgRequestTaskConfig}. Compile-time routing is the + * same as RAG/media: the tool name keys into ``ocgConfig`` at runtime, and + * the enrich script sets ``t.type`` to the configured task type. + */ + static final Set OCG_TOOL_TYPES = Set.of( + "ocg_query", + "ocg_get_entity", + "ocg_neighborhood", + "ocg_code_history", + "ocg_memory_set", + "ocg_memory_reinforce", + "ocg_memory_delete"); + /** Maps SDK tool type strings to Conductor task type strings. */ private static final Map TYPE_MAP = Map.ofEntries( Map.entry("worker", "SIMPLE"), @@ -98,7 +114,14 @@ private static Map escapeHeadersInConfig(Map cfg Map.entry("generate_video", "GENERATE_VIDEO"), Map.entry("rag_index", "LLM_INDEX_TEXT"), Map.entry("rag_search", "LLM_SEARCH_INDEX"), - Map.entry("pull_workflow_messages", "PULL_WORKFLOW_MESSAGES")); + Map.entry("pull_workflow_messages", "PULL_WORKFLOW_MESSAGES"), + Map.entry("ocg_query", "OCG_QUERY"), + Map.entry("ocg_get_entity", "OCG_GET_ENTITY"), + Map.entry("ocg_neighborhood", "OCG_NEIGHBORHOOD"), + Map.entry("ocg_code_history", "OCG_CODE_HISTORY"), + Map.entry("ocg_memory_set", "OCG_MEMORY_SET"), + Map.entry("ocg_memory_reinforce", "OCG_MEMORY_REINFORCE"), + Map.entry("ocg_memory_delete", "OCG_MEMORY_DELETE")); // ── Public API ─────────────────────────────────────────────────────── @@ -272,9 +295,10 @@ public Object[] buildEnrichTask(String agentName, String llmRef, List cliConfig = new LinkedHashMap<>(); Map humanConfig = new LinkedHashMap<>(); Map wmqConfig = new LinkedHashMap<>(); + Map ocgConfig = new LinkedHashMap<>(); if (tools != null) { - Set serverSideTypes = Set.of( + Set serverSideTypes = new HashSet<>(Set.of( "http", "mcp", "agent_tool", @@ -286,7 +310,8 @@ public Object[] buildEnrichTask(String agentName, String llmRef, List wmqEntry = new LinkedHashMap<>(); wmqEntry.put("batchSize", cfg.getOrDefault("batchSize", 1)); wmqConfig.put(tool.getName(), wmqEntry); + } else if (OCG_TOOL_TYPES.contains(toolType)) { + // OCG tools map to per-operation system tasks registered by + // OcgRequestTaskConfig. No defaults to carry — the OCG URL + // is resolved server-side from OcgProperties at bean + // construction time, so the script just needs ``taskType``. + Map ocgEntry = new LinkedHashMap<>(); + ocgEntry.put("taskType", TYPE_MAP.getOrDefault(toolType, toolType.toUpperCase())); + ocgConfig.put(tool.getName(), ocgEntry); } } } @@ -362,6 +395,7 @@ public Object[] buildEnrichTask(String agentName, String llmRef, List ragConfig = new LinkedHashMap<>(); Map humanConfig = new LinkedHashMap<>(); Map wmqConfig = new LinkedHashMap<>(); + Map ocgConfig = new LinkedHashMap<>(); if (tools != null) { for (ToolConfig tool : tools) { @@ -1506,6 +1550,10 @@ public Object[] buildEnrichTaskDynamic( Map wmqEntry = new LinkedHashMap<>(); wmqEntry.put("batchSize", cfg.getOrDefault("batchSize", 1)); wmqConfig.put(tool.getName(), wmqEntry); + } else if (OCG_TOOL_TYPES.contains(toolType)) { + Map ocgEntry = new LinkedHashMap<>(); + ocgEntry.put("taskType", TYPE_MAP.getOrDefault(toolType, toolType.toUpperCase())); + ocgConfig.put(tool.getName(), ocgEntry); } // MCP config comes from runtime — skip here } @@ -1517,6 +1565,7 @@ public Object[] buildEnrichTaskDynamic( String ragJson = JavaScriptBuilder.toJson(ragConfig); String humanJson = JavaScriptBuilder.toJson(humanConfig); String wmqJson = JavaScriptBuilder.toJson(wmqConfig); + String ocgJson = JavaScriptBuilder.toJson(ocgConfig); Map knownToolNames = new LinkedHashMap<>(); if (tools != null) { for (ToolConfig t : tools) { @@ -1525,7 +1574,7 @@ public Object[] buildEnrichTaskDynamic( } String knownToolNamesJson = JavaScriptBuilder.toJson(knownToolNames); String script = JavaScriptBuilder.enrichToolsScriptDynamic( - httpJson, mediaJson, agentToolJson, ragJson, humanJson, wmqJson, knownToolNamesJson); + httpJson, mediaJson, agentToolJson, ragJson, humanJson, wmqJson, ocgJson, knownToolNamesJson); String enrichRef = agentName + "_" + p + "enrich_tools"; diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java new file mode 100644 index 000000000..31159f393 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import dev.agentspan.runtime.model.AgentConfig; +import dev.agentspan.runtime.model.ToolConfig; + +/** + * Builds the {@link AgentConfig} for the server-registered OCG sub-agent. + * + *

The agent itself is a vanilla LLM-driven agent: it has instructions + * (the OCG retrieval prompt) and seven {@code ocg_*} tools that dispatch to + * the OCG_* system tasks registered by {@link OcgRequestTaskConfig}.

+ */ +public final class OcgAgentFactory { + + /** Stable workflow name for the registered OCG sub-agent. */ + public static final String AGENT_NAME = "_ocg_agent"; + + /** + * System prompt for the OCG sub-agent. Supplied verbatim by the feature + * spec — explains the retrieval/aggregation split and the two-step + * pattern callers must follow. + */ + static final String OCG_SYSTEM_PROMPT = "You are querying an OCG (Observability Context Graph). It is a RETRIEVAL\n" + + "engine over a knowledge graph of entities (messages, channels, people)\n" + + "linked by claims and relationships. It is NOT an aggregation engine.\n\n" + + "It can answer:\n" + + " - \"Find messages in channel X about Y\"\n" + + " - \"Show TIMED_OUT errors for cluster \"\n" + + " - \"What entities mention 'health check failure'?\"\n" + + " - \"Recent messages in #cloud_saas_health_check_alerts\"\n\n" + + "It CANNOT directly answer (you must do it yourself in two steps):\n" + + " - \"How many of X are there?\" / \"Which X is most frequent?\"\n" + + " - \"Group these by Y\" / \"Top N by count\"\n" + + " - Statistical or comparative questions\n\n" + + "For aggregation questions, use a TWO-STEP pattern:\n" + + " 1. RETRIEVE: ask OCG for the raw set of relevant entities.\n" + + " - Use specific terms (cluster names, error codes, channel names).\n" + + " - Use start_time / end_time in the request body to bound the range.\n" + + " - Set max_results high (e.g. 500) so you get the full set, not a\n" + + " top-N sample.\n" + + " - Avoid hedging words (\"frequently\", \"across\", \"occurrences\") —\n" + + " OCG ranks by keyword presence, and these are noise tokens.\n" + + " 2. AGGREGATE: count, group, rank yourself from the citation list.\n\n" + + "Query length: keep it under ~15 content words. Long prompts dilute the\n" + + "BM25 keyword set; OCG's parser is extracting things like \"happen\",\n" + + "\"identify\", \"top one\" which are not real signal.\n\n" + + "Bad: \"Across all clusters, what alert/notification/error type appears\n" + + " most frequently? Group similar alerts and tell me which one has\n" + + " the highest count and how many clusters it affected.\"\n\n" + + "Good (step 1): {\n" + + " \"query\": \"TIMED_OUT health check failure cluster\",\n" + + " \"max_results\": 500,\n" + + " \"start_time\": \"2026-05-04T00:00:00Z\",\n" + + " \"end_time\": \"2026-06-04T00:00:00Z\"\n" + + "}\n" + + "Then parse the returned citations, extract cluster names from titles,\n" + + "build the frequency table in your reasoning."; + + private OcgAgentFactory() {} + + public static AgentConfig build(OcgProperties props) { + return AgentConfig.builder() + .name(AGENT_NAME) + .description("Retrieval sub-agent over the Open Context Graph (OCG).") + .model(props.getModel()) + .instructions(OCG_SYSTEM_PROMPT) + .tools(buildTools()) + .maxTurns(10) + .build(); + } + + static List buildTools() { + return List.of( + queryTool(), + getEntityTool(), + neighborhoodTool(), + codeHistoryTool(), + memorySetTool(), + memoryReinforceTool(), + memoryDeleteTool()); + } + + private static ToolConfig queryTool() { + Map properties = new LinkedHashMap<>(); + properties.put("query", schema("string", "Natural-language retrieval query.")); + properties.put("max_results", schema("integer", "Max citations to return.", 10)); + properties.put( + "traversal_level", schema("integer", "0 = citations only, 1 = neighborhood, 2-3 = multi-hop.", 1)); + properties.put("start_time", schema("string", "ISO-8601 lower bound (inclusive). Optional.")); + properties.put("end_time", schema("string", "ISO-8601 upper bound (exclusive). Optional.")); + return ToolConfig.builder() + .name("ocg_query") + .toolType("ocg_query") + .description("Query the Open Context Graph for structured retrieval. " + + "Returns citations (source_item_id, title, container_id, snippet) " + + "and traversal_results when traversal_level > 0.") + .inputSchema(objectSchema(properties, List.of("query"))) + .build(); + } + + private static ToolConfig getEntityTool() { + Map properties = new LinkedHashMap<>(); + properties.put("entity_id", schema("string", "Canonical entity id from an ocg_query result row.")); + return ToolConfig.builder() + .name("ocg_get_entity") + .toolType("ocg_get_entity") + .description("Fetch one entity by its canonical id.") + .inputSchema(objectSchema(properties, List.of("entity_id"))) + .build(); + } + + private static ToolConfig neighborhoodTool() { + Map properties = new LinkedHashMap<>(); + properties.put("entity_id", schema("string", "Entity at the center of the neighborhood.")); + properties.put("depth", schema("integer", "Hop depth (use depth=1 on first call).", 2)); + properties.put("limit", schema("integer", "Cap on neighbors returned (use <= 10 on first call).", 50)); + return ToolConfig.builder() + .name("ocg_neighborhood") + .toolType("ocg_neighborhood") + .description("Get an entity plus its graph neighbors out to `depth` hops. " + + "Use limit <= 10, depth=1 on the first call — well-connected " + + "entities can have many edges and large responses will be truncated.") + .inputSchema(objectSchema(properties, List.of("entity_id"))) + .build(); + } + + private static ToolConfig codeHistoryTool() { + Map properties = new LinkedHashMap<>(); + properties.put("repo_id", schema("string", "Ingested repository id.")); + properties.put("path", schema("string", "Path within the repo.")); + properties.put("limit", schema("integer", "Max commits to return.", 20)); + return ToolConfig.builder() + .name("ocg_code_history") + .toolType("ocg_code_history") + .description("Last N commits that touched a file in an ingested repo.") + .inputSchema(objectSchema(properties, List.of("repo_id", "path"))) + .build(); + } + + private static ToolConfig memorySetTool() { + Map properties = new LinkedHashMap<>(); + properties.put("key", schema("string", "Memory key.")); + properties.put("agent", schema("string", "Agent owner (e.g. \"agent:\").")); + properties.put("user", schema("string", "User owner (e.g. \"user:\").")); + properties.put("string_value", schema("string", "Stored value.")); + properties.put("description", schema("string", "Human-readable description.")); + properties.put( + "scope", + schema( + "string", + "Memory scope. One of MEMORY_SCOPE_SESSION, MEMORY_SCOPE_AGENT, MEMORY_SCOPE_USER, MEMORY_SCOPE_SHARED, MEMORY_SCOPE_GLOBAL.", + "MEMORY_SCOPE_USER")); + properties.put("confidence", schema("number", "Inferred confidence in [0,1]. Cap at 0.7.", 0.7)); + properties.put("source_ref", schema("string", "Free-form source reference (e.g. message id).")); + properties.put("evidence_ids", arraySchema("string", "Supporting evidence entity ids.")); + properties.put("tags", arraySchema("string", "Tags.")); + properties.put("expires_at", schema("string", "ISO-8601 expiry. Optional — default 180 days.")); + properties.put("idempotency_key", schema("string", "Idempotency key. Optional.")); + return ToolConfig.builder() + .name("ocg_memory_set") + .toolType("ocg_memory_set") + .description("Create or overwrite a memory in OCG. Cap inferred confidence at 0.7; " + + "never write PII or secrets.") + .inputSchema(objectSchema(properties, List.of("key", "agent", "user", "string_value", "description"))) + .build(); + } + + private static ToolConfig memoryReinforceTool() { + Map properties = new LinkedHashMap<>(); + properties.put("key", schema("string", "Memory key.")); + properties.put("agent", schema("string", "Agent owner.")); + properties.put("user", schema("string", "User owner.")); + properties.put( + "confidence_boost", + schema("number", "Boost to add (must be <= 0.05 to prevent compounding drift).", 0.05)); + properties.put("source_ref", schema("string", "Free-form source reference.")); + return ToolConfig.builder() + .name("ocg_memory_reinforce") + .toolType("ocg_memory_reinforce") + .description("Reinforce an existing memory on independent re-observation. " + + "confidence_boost must be <= 0.05.") + .inputSchema(objectSchema(properties, List.of("key", "agent", "user"))) + .build(); + } + + private static ToolConfig memoryDeleteTool() { + Map properties = new LinkedHashMap<>(); + properties.put("key", schema("string", "Memory key.")); + properties.put("agent", schema("string", "Agent owner.")); + properties.put("user", schema("string", "User owner.")); + return ToolConfig.builder() + .name("ocg_memory_delete") + .toolType("ocg_memory_delete") + .description("Delete a memory by key. Prefer ocg_memory_set with a corrected value " + + "over deletion (preserves history).") + .inputSchema(objectSchema(properties, List.of("key", "agent", "user"))) + .build(); + } + + // ───────────────────────────────────────────────────────────────────── + // JSON-schema helpers + // ───────────────────────────────────────────────────────────────────── + + private static Map objectSchema(Map properties, List required) { + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + schema.put("properties", properties); + if (!required.isEmpty()) { + schema.put("required", required); + } + return schema; + } + + private static Map schema(String type, String description) { + Map s = new LinkedHashMap<>(); + s.put("type", type); + s.put("description", description); + return s; + } + + private static Map schema(String type, String description, Object defaultValue) { + Map s = schema(type, description); + s.put("default", defaultValue); + return s; + } + + private static Map arraySchema(String itemType, String description) { + Map s = new LinkedHashMap<>(); + s.put("type", "array"); + s.put("description", description); + s.put("items", Map.of("type", itemType)); + return s; + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentToolInjector.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentToolInjector.java new file mode 100644 index 000000000..c56f9bec1 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentToolInjector.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import dev.agentspan.runtime.model.AgentConfig; +import dev.agentspan.runtime.model.ToolConfig; + +/** + * Silently appends the OCG sub-agent as an {@code agent_tool} on the top-level + * {@link AgentConfig} when OCG is enabled. + * + *

Extracted from {@code AgentService} as a static helper so the injection + * rules (skip self, skip duplicates, never touch nested sub-agents) are + * unit-testable without standing up the full service.

+ */ +public final class OcgAgentToolInjector { + + public static final String OCG_AGENT_TOOL_NAME = "ocg_agent"; + + /** + * Description shown to the main agent's LLM in its tool spec list. Keep + * it concrete enough that the model can decide *when* to delegate, but + * short enough not to waste tokens. + */ + static final String OCG_AGENT_TOOL_DESCRIPTION = + "Delegate to the OCG (Open Context Graph) retrieval agent when you need " + + "context from the knowledge graph — message search, entity lookup, " + + "code history, or stored memories. Provide a focused natural-language " + + "query (under ~15 content words). Returns a synthesized answer with " + + "supporting citations."; + + private OcgAgentToolInjector() {} + + /** + * Returns {@code config} with an {@code ocg_agent} tool appended if and + * only if all of the following hold: + *
    + *
  • {@code ocgEnabled} is true
  • + *
  • {@code config} is not the registered {@code _ocg_agent} itself (no self-recursion)
  • + *
  • {@code config} doesn't already declare a tool named {@code ocg_agent}
  • + *
+ * Otherwise returns {@code config} unchanged. + * + *

Nested sub-agents are intentionally untouched — only the top-level + * agent receives the injection, so a specialist sub-agent isn't polluted + * with an unrelated retrieval tool.

+ */ + public static AgentConfig inject(AgentConfig config, boolean ocgEnabled) { + if (config == null || !ocgEnabled) return config; + if (OcgAgentFactory.AGENT_NAME.equals(config.getName())) return config; + + List existing = config.getTools(); + if (existing != null) { + for (ToolConfig t : existing) { + if (OCG_AGENT_TOOL_NAME.equals(t.getName())) { + return config; + } + } + } + List merged = new ArrayList<>(); + if (existing != null) merged.addAll(existing); + merged.add(ToolConfig.builder() + .name(OCG_AGENT_TOOL_NAME) + .toolType("agent_tool") + .description(OCG_AGENT_TOOL_DESCRIPTION) + .config(Map.of("workflowName", OcgAgentFactory.AGENT_NAME)) + .build()); + config.setTools(merged); + return config; + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java new file mode 100644 index 000000000..d4ce8bfd8 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.Data; + +/** + * Configuration for the optional OCG (Open Context Graph) sub-agent. + * + *

When {@code agentspan.ocg.url} is set, the server registers a specialized + * retrieval sub-agent at startup, exposes seven {@code OCG_*} system tasks + * that make HTTP calls to OCG with response capping + field projection, and + * auto-injects {@code _ocg_agent} as an {@code agent_tool} into every + * top-level agent so the main agent's LLM can decide to delegate to OCG + * when it needs context.

+ */ +@Data +@ConfigurationProperties(prefix = "agentspan.ocg") +public class OcgProperties { + + /** Base URL of the OCG service. Empty / null disables the entire OCG feature. */ + private String url; + + /** Model used by the OCG sub-agent's LLM turns. */ + private String model = "openai/gpt-4o-mini"; + + /** + * Per-response truncation cap (post-projection, JSON-serialized) for the + * {@code OCG_*} system tasks. Mirrors the Python reference helper + * {@code _enforce_response_cap}. + */ + private int responseCapChars = 8192; + + public boolean isEnabled() { + return url != null && !url.isBlank(); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java new file mode 100644 index 000000000..b417d89bc --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.conductor.core.execution.WorkflowExecutor; +import com.netflix.conductor.core.execution.tasks.WorkflowSystemTask; +import com.netflix.conductor.model.TaskModel; +import com.netflix.conductor.model.WorkflowModel; + +/** + * System task that proxies a single OCG (Open Context Graph) operation. + * + *

One {@link OcgRequestTask} instance is registered per OCG endpoint via + * {@link OcgRequestTaskConfig}. The task type strings (e.g. {@code OCG_QUERY}, + * {@code OCG_GET_ENTITY}) are stable contracts the OCG sub-agent's tool calls + * dispatch to.

+ * + *

Each {@link #start} call:

+ *
    + *
  1. Reads operation-specific arguments from {@code task.inputData}
  2. + *
  3. Issues an HTTP request to OCG (path + method determined by operation)
  4. + *
  5. Projects the response to the fields the LLM actually needs
  6. + *
  7. Caps the JSON-serialized projection to + * {@link OcgProperties#getResponseCapChars()} so a 5MB graph traversal + * can't blow the model's context window
  8. + *
+ * + *

HTTP I/O uses the same {@link HttpClient} pattern as + * {@code PlannerContextFetchTask} — synchronous, with sensible timeouts, and + * a constructor-injection seam for unit testing without the network.

+ */ +public class OcgRequestTask extends WorkflowSystemTask { + + public static final String OP_QUERY = "query"; + public static final String OP_GET_ENTITY = "get_entity"; + public static final String OP_NEIGHBORHOOD = "neighborhood"; + public static final String OP_CODE_HISTORY = "code_history"; + public static final String OP_MEMORY_SET = "memory_set"; + public static final String OP_MEMORY_REINFORCE = "memory_reinforce"; + public static final String OP_MEMORY_DELETE = "memory_delete"; + + private static final Logger logger = LoggerFactory.getLogger(OcgRequestTask.class); + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10); + private static final Duration READ_TIMEOUT = Duration.ofSeconds(30); + private static final String TRUNCATE_SUFFIX = "...[truncated]"; + + private final String operation; + private final OcgProperties properties; + private final HttpClient httpClient; + + public OcgRequestTask(String taskType, String operation, OcgProperties properties) { + this( + taskType, + operation, + properties, + HttpClient.newBuilder() + .connectTimeout(CONNECT_TIMEOUT) + .followRedirects(HttpClient.Redirect.NORMAL) + .build()); + } + + /** Visible-for-testing constructor with an injectable {@link HttpClient}. */ + OcgRequestTask(String taskType, String operation, OcgProperties properties, HttpClient httpClient) { + super(taskType); + this.operation = Objects.requireNonNull(operation, "operation"); + this.properties = Objects.requireNonNull(properties, "properties"); + this.httpClient = Objects.requireNonNull(httpClient, "httpClient"); + logger.debug("OcgRequestTask registered (taskType={}, operation={})", taskType, operation); + } + + @Override + public void start(WorkflowModel workflow, TaskModel task, WorkflowExecutor executor) { + if (!properties.isEnabled()) { + fail(task, "OCG is not configured (agentspan.ocg.url is empty)"); + return; + } + + Map input = task.getInputData() == null ? Map.of() : task.getInputData(); + + try { + HttpRequest request = buildRequest(input); + HttpResponse resp = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + int status = resp.statusCode(); + if (status < 200 || status >= 300) { + fail(task, "OCG " + operation + " returned status " + status + ": " + truncateForLog(resp.body())); + return; + } + Object parsed = parseJsonLenient(resp.body()); + Object projected = project(parsed); + String serialized = MAPPER.writeValueAsString(projected); + if (serialized.length() > properties.getResponseCapChars()) { + int cap = Math.max(0, properties.getResponseCapChars() - TRUNCATE_SUFFIX.length()); + serialized = serialized.substring(0, cap) + TRUNCATE_SUFFIX; + } + Map output = new LinkedHashMap<>(); + output.put("result", serialized); + output.put("operation", operation); + task.setOutputData(output); + task.setStatus(TaskModel.Status.COMPLETED); + } catch (Exception e) { + fail(task, "OCG " + operation + " failed: " + e.getMessage()); + } + } + + // ───────────────────────────────────────────────────────────────────── + // Operation → HTTP request mapping + // ───────────────────────────────────────────────────────────────────── + + HttpRequest buildRequest(Map input) throws Exception { + String baseUrl = trimTrailingSlash(properties.getUrl()); + HttpRequest.Builder b = HttpRequest.newBuilder().timeout(READ_TIMEOUT).header("Accept", "application/json"); + + switch (operation) { + case OP_QUERY -> { + Map body = new LinkedHashMap<>(); + copyIfPresent(input, body, "query"); + copyIfPresent(input, body, "max_results"); + copyIfPresent(input, body, "traversal_level"); + copyIfPresent(input, body, "start_time"); + copyIfPresent(input, body, "end_time"); + return b.uri(URI.create(baseUrl + "/agent/query")) + .header("Content-Type", "application/json") + .POST(BodyPublishers.ofString(MAPPER.writeValueAsString(body), StandardCharsets.UTF_8)) + .build(); + } + case OP_GET_ENTITY -> { + String entityId = requiredString(input, "entity_id"); + return b.uri(URI.create(baseUrl + "/entities/" + urlEncode(entityId))) + .GET() + .build(); + } + case OP_NEIGHBORHOOD -> { + String entityId = requiredString(input, "entity_id"); + String query = "?depth=" + intOr(input.get("depth"), 2) + "&limit=" + intOr(input.get("limit"), 50); + return b.uri(URI.create(baseUrl + "/graph/neighborhood/" + urlEncode(entityId) + query)) + .GET() + .build(); + } + case OP_CODE_HISTORY -> { + String repoId = requiredString(input, "repo_id"); + String path = requiredString(input, "path"); + String query = "?path=" + urlEncode(path) + "&limit=" + intOr(input.get("limit"), 20); + return b.uri(URI.create(baseUrl + "/code/history/" + urlEncode(repoId) + query)) + .GET() + .build(); + } + case OP_MEMORY_SET -> { + Map body = new LinkedHashMap<>(input); + body.remove("__agentspan_ctx__"); + return b.uri(URI.create(baseUrl + "/memories")) + .header("Content-Type", "application/json") + .POST(BodyPublishers.ofString(MAPPER.writeValueAsString(body), StandardCharsets.UTF_8)) + .build(); + } + case OP_MEMORY_REINFORCE -> { + String key = requiredString(input, "key"); + Map body = new LinkedHashMap<>(); + copyIfPresent(input, body, "agent"); + copyIfPresent(input, body, "user"); + copyIfPresent(input, body, "confidence_boost"); + copyIfPresent(input, body, "source_ref"); + return b.uri(URI.create(baseUrl + "/memories/" + urlEncode(key) + "/reinforce")) + .header("Content-Type", "application/json") + .POST(BodyPublishers.ofString(MAPPER.writeValueAsString(body), StandardCharsets.UTF_8)) + .build(); + } + case OP_MEMORY_DELETE -> { + String key = requiredString(input, "key"); + String agent = stringOrEmpty(input.get("agent")); + String user = stringOrEmpty(input.get("user")); + StringBuilder q = new StringBuilder(); + if (!agent.isEmpty()) q.append("agent=").append(urlEncode(agent)); + if (!user.isEmpty()) { + if (q.length() > 0) q.append('&'); + q.append("user=").append(urlEncode(user)); + } + String suffix = q.length() > 0 ? "?" + q : ""; + return b.uri(URI.create(baseUrl + "/memories/" + urlEncode(key) + suffix)) + .DELETE() + .build(); + } + default -> throw new IllegalStateException("Unsupported OCG operation: " + operation); + } + } + + // ───────────────────────────────────────────────────────────────────── + // Response projection (mirrors Python `_project_*` helpers) + // ───────────────────────────────────────────────────────────────────── + + @SuppressWarnings("unchecked") + Object project(Object raw) { + if (!(raw instanceof Map)) return raw; + Map map = (Map) raw; + return switch (operation) { + case OP_QUERY -> projectQuery(map); + case OP_GET_ENTITY -> projectEntity(map); + case OP_NEIGHBORHOOD -> projectNeighborhood(map); + case OP_CODE_HISTORY -> projectCodeHistory(map); + default -> map; // memory_* — pass through + }; + } + + @SuppressWarnings("unchecked") + private Map projectQuery(Map raw) { + Map out = new LinkedHashMap<>(); + Object citations = raw.get("citations"); + List> projectedCitations = new ArrayList<>(); + if (citations instanceof List list) { + for (Object item : list) { + if (item instanceof Map c) { + Map cit = (Map) c; + Map projected = new LinkedHashMap<>(); + copyIfPresent(cit, projected, "source_item_id"); + copyIfPresent(cit, projected, "title"); + copyIfPresent(cit, projected, "container_id"); + copyIfPresent(cit, projected, "snippet"); + projectedCitations.add(projected); + } + } + } + out.put("citations", projectedCitations); + if (raw.containsKey("traversal_results")) { + out.put("traversal_results", raw.get("traversal_results")); + } + return out; + } + + private Map projectEntity(Map raw) { + Map out = new LinkedHashMap<>(); + copyIfPresent(raw, out, "id"); + copyIfPresent(raw, out, "type"); + copyIfPresent(raw, out, "title"); + copyIfPresent(raw, out, "properties"); + return out; + } + + private Map projectNeighborhood(Map raw) { + Map out = new LinkedHashMap<>(); + copyIfPresent(raw, out, "center"); + copyIfPresent(raw, out, "edges"); + copyIfPresent(raw, out, "neighbors"); + return out; + } + + private Map projectCodeHistory(Map raw) { + Map out = new LinkedHashMap<>(); + copyIfPresent(raw, out, "commits"); + copyIfPresent(raw, out, "repo_id"); + copyIfPresent(raw, out, "path"); + return out; + } + + // ───────────────────────────────────────────────────────────────────── + // Helpers + // ───────────────────────────────────────────────────────────────────── + + private static String trimTrailingSlash(String url) { + if (url == null) return ""; + return url.endsWith("/") ? url.substring(0, url.length() - 1) : url; + } + + private static String urlEncode(String v) { + return URLEncoder.encode(v, StandardCharsets.UTF_8); + } + + private static void copyIfPresent(Map src, Map dst, String key) { + if (src.containsKey(key) && src.get(key) != null) { + dst.put(key, src.get(key)); + } + } + + private static String requiredString(Map input, String key) { + Object v = input.get(key); + if (!(v instanceof String s) || s.isBlank()) { + throw new IllegalArgumentException("Missing or empty required input '" + key + "'"); + } + return s; + } + + private static String stringOrEmpty(Object v) { + return v == null ? "" : String.valueOf(v); + } + + private static int intOr(Object v, int fallback) { + if (v instanceof Number n) return n.intValue(); + if (v instanceof String s) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException ignored) { + return fallback; + } + } + return fallback; + } + + private static Object parseJsonLenient(String body) { + if (body == null || body.isBlank()) return Map.of(); + try { + return MAPPER.readValue(body, Object.class); + } catch (Exception e) { + // Non-JSON bodies (rare) — surface the raw text under a known key. + return Map.of("raw", body); + } + } + + private static String truncateForLog(String body) { + if (body == null) return ""; + return body.length() > 256 ? body.substring(0, 256) + "..." : body; + } + + private static void fail(TaskModel task, String reason) { + Map out = new LinkedHashMap<>(); + out.put("error", reason); + task.setOutputData(out); + task.setReasonForIncompletion(reason); + task.setStatus(TaskModel.Status.FAILED); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java new file mode 100644 index 000000000..32db7beab --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Registers the seven {@code OCG_*} system tasks as Conductor beans when + * {@code agentspan.ocg.url} is set. Bean names match the Conductor task type + * strings so the framework's {@code SystemTaskRegistry} looks them up by type. + */ +@Configuration +@EnableConfigurationProperties(OcgProperties.class) +@ConditionalOnProperty(prefix = "agentspan.ocg", name = "url") +public class OcgRequestTaskConfig { + + public static final String TASK_TYPE_QUERY = "OCG_QUERY"; + public static final String TASK_TYPE_GET_ENTITY = "OCG_GET_ENTITY"; + public static final String TASK_TYPE_NEIGHBORHOOD = "OCG_NEIGHBORHOOD"; + public static final String TASK_TYPE_CODE_HISTORY = "OCG_CODE_HISTORY"; + public static final String TASK_TYPE_MEMORY_SET = "OCG_MEMORY_SET"; + public static final String TASK_TYPE_MEMORY_REINFORCE = "OCG_MEMORY_REINFORCE"; + public static final String TASK_TYPE_MEMORY_DELETE = "OCG_MEMORY_DELETE"; + + @Bean(TASK_TYPE_QUERY) + public OcgRequestTask ocgQueryTask(OcgProperties props) { + return new OcgRequestTask(TASK_TYPE_QUERY, OcgRequestTask.OP_QUERY, props); + } + + @Bean(TASK_TYPE_GET_ENTITY) + public OcgRequestTask ocgGetEntityTask(OcgProperties props) { + return new OcgRequestTask(TASK_TYPE_GET_ENTITY, OcgRequestTask.OP_GET_ENTITY, props); + } + + @Bean(TASK_TYPE_NEIGHBORHOOD) + public OcgRequestTask ocgNeighborhoodTask(OcgProperties props) { + return new OcgRequestTask(TASK_TYPE_NEIGHBORHOOD, OcgRequestTask.OP_NEIGHBORHOOD, props); + } + + @Bean(TASK_TYPE_CODE_HISTORY) + public OcgRequestTask ocgCodeHistoryTask(OcgProperties props) { + return new OcgRequestTask(TASK_TYPE_CODE_HISTORY, OcgRequestTask.OP_CODE_HISTORY, props); + } + + @Bean(TASK_TYPE_MEMORY_SET) + public OcgRequestTask ocgMemorySetTask(OcgProperties props) { + return new OcgRequestTask(TASK_TYPE_MEMORY_SET, OcgRequestTask.OP_MEMORY_SET, props); + } + + @Bean(TASK_TYPE_MEMORY_REINFORCE) + public OcgRequestTask ocgMemoryReinforceTask(OcgProperties props) { + return new OcgRequestTask(TASK_TYPE_MEMORY_REINFORCE, OcgRequestTask.OP_MEMORY_REINFORCE, props); + } + + @Bean(TASK_TYPE_MEMORY_DELETE) + public OcgRequestTask ocgMemoryDeleteTask(OcgProperties props) { + return new OcgRequestTask(TASK_TYPE_MEMORY_DELETE, OcgRequestTask.OP_MEMORY_DELETE, props); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgSubAgentService.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgSubAgentService.java new file mode 100644 index 000000000..628232f06 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgSubAgentService.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import jakarta.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import com.netflix.conductor.common.metadata.workflow.WorkflowDef; +import com.netflix.conductor.dao.MetadataDAO; + +import dev.agentspan.runtime.compiler.AgentCompiler; +import dev.agentspan.runtime.model.AgentConfig; + +import lombok.RequiredArgsConstructor; + +/** + * Registers the OCG sub-agent workflow at startup (when + * {@code agentspan.ocg.url} is set). + * + *

The agent itself is built by {@link OcgAgentFactory}, compiled by the + * standard {@link AgentCompiler}, and persisted via {@link MetadataDAO}. From + * Conductor's perspective it is just another workflow named + * {@code _ocg_agent}.

+ * + *

Main-agent invocation happens via an {@code agent_tool} that + * {@code AgentService} auto-injects into every top-level agent — the main + * agent's LLM decides whether and when to call it. This service no longer + * dispatches OCG itself; it only owns the workflow's registration.

+ */ +@Service +@ConditionalOnProperty(prefix = "agentspan.ocg", name = "url") +@RequiredArgsConstructor +public class OcgSubAgentService { + + private static final Logger log = LoggerFactory.getLogger(OcgSubAgentService.class); + + private final OcgProperties properties; + private final AgentCompiler agentCompiler; + private final MetadataDAO metadataDAO; + + @PostConstruct + public void registerWorkflow() { + if (!properties.isEnabled()) { + // Defensive — the @ConditionalOnProperty guard means we shouldn't + // be here, but keep the check so unit tests can instantiate this + // service directly with a disabled config without crashing. + return; + } + AgentConfig config = OcgAgentFactory.build(properties); + WorkflowDef def = agentCompiler.compile(config); + metadataDAO.updateWorkflowDef(def); + log.info( + "OCG sub-agent registered: workflow='{}' model='{}' url='{}'", + def.getName(), + properties.getModel(), + properties.getUrl()); + } + + public boolean isEnabled() { + return properties.isEnabled(); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/service/AgentService.java b/server/src/main/java/dev/agentspan/runtime/service/AgentService.java index 26f822f6e..2867d6a62 100644 --- a/server/src/main/java/dev/agentspan/runtime/service/AgentService.java +++ b/server/src/main/java/dev/agentspan/runtime/service/AgentService.java @@ -47,6 +47,9 @@ import dev.agentspan.runtime.credentials.ExecutionTokenService; import dev.agentspan.runtime.model.*; import dev.agentspan.runtime.normalizer.NormalizerRegistry; +import dev.agentspan.runtime.ocg.OcgAgentFactory; +import dev.agentspan.runtime.ocg.OcgAgentToolInjector; +import dev.agentspan.runtime.ocg.OcgSubAgentService; import dev.agentspan.runtime.util.ModelParser; import dev.agentspan.runtime.util.ProviderValidator; @@ -75,6 +78,9 @@ public class AgentService { @Autowired(required = false) private SkillRegistryService skillRegistryService; + @Autowired(required = false) + private OcgSubAgentService ocgSubAgentService; + /** Package-private constructor for testing with ExecutionTokenService */ AgentService( AgentCompiler agentCompiler, @@ -1161,6 +1167,7 @@ private Optional validateModelProvider(AgentConfig config) { * Otherwise, use the native {@code agentConfig} field directly. */ private AgentConfig resolveConfig(StartRequest request) { + AgentConfig config; if (request.getFramework() != null && !request.getFramework().isEmpty()) { log.info("Normalizing framework '{}' agent config", request.getFramework()); if ("skill".equals(request.getFramework()) @@ -1171,9 +1178,41 @@ private AgentConfig resolveConfig(StartRequest request) { } request.setRawConfig(skillRegistryService.resolveRawConfig(request.getSkillRef())); } - return normalizerRegistry.normalize(request.getFramework(), request.getRawConfig()); + config = normalizerRegistry.normalize(request.getFramework(), request.getRawConfig()); + } else { + config = request.getAgentConfig(); + } + return maybeInjectOcgAgentTool(config); + } + + /** + * Silently append the OCG sub-agent as an {@code agent_tool} on the + * top-level config when OCG is enabled. Pre-flight has been removed — + * the main agent's LLM decides whether to call OCG. + * + *

Logic lives in {@link OcgAgentToolInjector} so the rules (skip + * self, skip duplicates, only top-level) are unit-testable without the + * full service stack.

+ */ + private AgentConfig maybeInjectOcgAgentTool(AgentConfig config) { + boolean enabled = ocgSubAgentService != null && ocgSubAgentService.isEnabled(); + AgentConfig result = OcgAgentToolInjector.inject(config, enabled); + if (enabled + && result != null + && result.getTools() != null + && !result.getTools().isEmpty() + && OcgAgentToolInjector.OCG_AGENT_TOOL_NAME.equals( + result.getTools().get(result.getTools().size() - 1).getName()) + && !OcgAgentFactory.AGENT_NAME.equals(result.getName())) { + // Logged only when injection actually happened on this call (last + // tool == ocg_agent and we aren't compiling the OCG agent itself). + // Duplicate-skip and self-skip paths stay silent. + log.info( + "Auto-injected '{}' agent_tool into '{}'", + OcgAgentToolInjector.OCG_AGENT_TOOL_NAME, + config != null ? config.getName() : ""); } - return request.getAgentConfig(); + return result; } // ── SSE Streaming ────────────────────────────────────────────── diff --git a/server/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java b/server/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java index 1c0999bb4..1ad4554e0 100644 --- a/server/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java +++ b/server/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java @@ -526,6 +526,7 @@ public static String enrichToolsScript( String cliConfigJson, String humanConfigJson, String wmqConfigJson, + String ocgConfigJson, String knownToolNamesJson) { return iife(" var httpCfg = " + httpConfigJson + ";" + " var mcpCfg = " + mcpConfigJson + ";" + " var mediaCfg = " @@ -534,7 +535,8 @@ public static String enrichToolsScript( + ragConfigJson + ";" + " var cliCfg = " + cliConfigJson + ";" + " var humanCfg = " + humanConfigJson + ";" + " var wmqCfg = " - + wmqConfigJson + ";" + " var knownNames = " + knownToolNamesJson + ";" + + wmqConfigJson + ";" + " var ocgCfg = " + + ocgConfigJson + ";" + " var knownNames = " + knownToolNamesJson + ";" + " var agentState = $.agentState || {};" + " var tcs = $.toolCalls || [];" + " var result = [];" @@ -546,7 +548,7 @@ public static String enrichToolsScript( // SIMPLE task gets queued under the unknown name with no worker // polling for it and the workflow hangs forever. + " var isCfg = !!(httpCfg[n] || mcpCfg[n] || agentToolCfg[n] ||" - + " mediaCfg[n] || ragCfg[n] || humanCfg[n] || wmqCfg[n]);" + + " mediaCfg[n] || ragCfg[n] || humanCfg[n] || wmqCfg[n] || ocgCfg[n]);" // Reject any name not in the agent's declared tools. The // previous gate (``hasKnownNames``) skipped this check when // ``knownNames`` was empty, which allowed an agent declared @@ -635,6 +637,15 @@ public static String enrichToolsScript( + " var inp = tc.inputParameters || {};" + " for (var k in inp) { merged[k] = inp[k]; }" + " t.inputParameters = merged;" + + " } else if (ocgCfg[n]) {" + // OCG tools dispatch to per-operation OCG_* system tasks. The + // OCG URL is resolved server-side from OcgProperties, so the + // script only needs to set the task type and forward the + // LLM-supplied arguments verbatim as inputParameters. + + " t.type = ocgCfg[n].taskType;" + + " t.name = ocgCfg[n].taskType.toLowerCase();" + + " t.inputParameters = tc.inputParameters || {};" + + " if ($.agentspanCtx) { t.inputParameters.__agentspan_ctx__ = $.agentspanCtx; }" + " } else if (humanCfg[n]) {" + " t.type = 'HUMAN';" + " t.name = n;" @@ -1119,6 +1130,7 @@ public static String enrichToolsScriptDynamic( String ragConfigJson, String humanConfigJson, String wmqConfigJson, + String ocgConfigJson, String knownToolNamesJson) { return iife(" var httpCfg = " + httpConfigJson + ";" + " var mcpCfg = $.mcpConfig || {};" + " var apiCfg = $.apiConfig || {};" @@ -1127,7 +1139,8 @@ public static String enrichToolsScriptDynamic( + agentToolConfigJson + ";" + " var ragCfg = " + ragConfigJson + ";" + " var humanCfg = " + humanConfigJson + ";" + " var wmqCfg = " - + wmqConfigJson + ";" + " var knownNames = " + knownToolNamesJson + ";" + + wmqConfigJson + ";" + " var ocgCfg = " + + ocgConfigJson + ";" + " var knownNames = " + knownToolNamesJson + ";" + " var agentState = $.agentState || {};" + " var tcs = $.toolCalls || [];" + " var result = [];" @@ -1137,7 +1150,7 @@ public static String enrichToolsScriptDynamic( // for context). Without this the SIMPLE task gets queued under // an unknown name and the workflow hangs forever. + " var isCfg = !!(httpCfg[n] || mcpCfg[n] || apiCfg[n] || agentToolCfg[n] ||" - + " mediaCfg[n] || ragCfg[n] || humanCfg[n] || wmqCfg[n]);" + + " mediaCfg[n] || ragCfg[n] || humanCfg[n] || wmqCfg[n] || ocgCfg[n]);" // See ``enrichToolsScript`` above — empty knownNames means // NO tool is callable by the LLM (locks down the prefill-only // leak path). @@ -1254,6 +1267,11 @@ public static String enrichToolsScriptDynamic( + " var inp = tc.inputParameters || {};" + " for (var k in inp) { merged[k] = inp[k]; }" + " t.inputParameters = merged;" + + " } else if (ocgCfg[n]) {" + + " t.type = ocgCfg[n].taskType;" + + " t.name = ocgCfg[n].taskType.toLowerCase();" + + " t.inputParameters = tc.inputParameters || {};" + + " if ($.agentspanCtx) { t.inputParameters.__agentspan_ctx__ = $.agentspanCtx; }" + " } else if (humanCfg[n]) {" + " t.type = 'HUMAN';" + " t.name = n;" diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 83d9e1f6a..abe167be5 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -174,6 +174,18 @@ agentspan.credentials.resolve.rate-limit=120 # spring.sql.init.mode=always # spring.sql.init.schema-locations=classpath:schema-secrets.sql +# ============================================================================= +# OCG (Open Context Graph) Configuration +# ============================================================================= +# Set OCG_URL to enable the OCG sub-agent. When set: +# - 7 OCG_* system tasks are registered +# - The _ocg_agent workflow is registered at startup +# - Every top-level agent is silently given an "ocg_agent" agent_tool, +# so the main agent's LLM can delegate to OCG when it needs context. +agentspan.ocg.url=${OCG_URL:} +agentspan.ocg.model=${OCG_MODEL:openai/gpt-4o-mini} +agentspan.ocg.response-cap-chars=8192 + # Metrics conductor.metrics-prometheus.enabled=true management.endpoints.web.exposure.include=health,info,prometheus diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java new file mode 100644 index 000000000..571a0145a --- /dev/null +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import dev.agentspan.runtime.model.AgentConfig; +import dev.agentspan.runtime.model.ToolConfig; + +/** + * Unit tests for {@link OcgAgentFactory}. + * + *

Pins the agent's surface area — name, model wiring, tool count, tool + * name → toolType mapping — so a refactor cannot silently drop one of the + * seven OCG operations or rename an operation away from its registered + * {@code OCG_*} task type.

+ */ +class OcgAgentFactoryTest { + + private static OcgProperties props() { + OcgProperties p = new OcgProperties(); + p.setUrl("http://ocg.local"); + p.setModel("openai/gpt-4o-mini"); + return p; + } + + @Test + void builtAgentCarriesNameModelAndSystemPrompt() { + AgentConfig cfg = OcgAgentFactory.build(props()); + + assertThat(cfg.getName()).isEqualTo("_ocg_agent"); + assertThat(cfg.getModel()).isEqualTo("openai/gpt-4o-mini"); + // System prompt must include the retrieval/aggregation distinction so + // model behaviour stays aligned with the documented OCG contract. + assertThat(cfg.getInstructions().toString()) + .contains("RETRIEVAL") + .contains("aggregation") + .contains("TWO-STEP"); + } + + @Test + void exposesAllSevenOcgToolsWithMatchingToolTypes() { + List tools = OcgAgentFactory.build(props()).getTools(); + assertThat(tools).hasSize(7); + + // Each tool's `name` must match its `toolType` so ToolCompiler's + // TYPE_MAP lookup resolves to the right OCG_* task type. A drift here + // would silently route the tool call to a SIMPLE task with no worker. + for (ToolConfig t : tools) { + assertThat(t.getName()) + .as("tool name == toolType (so TYPE_MAP routes correctly)") + .isEqualTo(t.getToolType()); + } + + List names = tools.stream().map(ToolConfig::getName).toList(); + assertThat(names) + .containsExactlyInAnyOrder( + "ocg_query", + "ocg_get_entity", + "ocg_neighborhood", + "ocg_code_history", + "ocg_memory_set", + "ocg_memory_reinforce", + "ocg_memory_delete"); + } + + @Test + void queryToolDeclaresQueryAsRequiredInput() { + ToolConfig queryTool = OcgAgentFactory.build(props()).getTools().stream() + .filter(t -> "ocg_query".equals(t.getName())) + .findFirst() + .orElseThrow(); + Object required = queryTool.getInputSchema().get("required"); + assertThat(required).asList().contains("query"); + } +} diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentToolInjectorTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentToolInjectorTest.java new file mode 100644 index 000000000..dff7336e8 --- /dev/null +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentToolInjectorTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import dev.agentspan.runtime.model.AgentConfig; +import dev.agentspan.runtime.model.ToolConfig; + +/** + * Unit tests pinning {@link OcgAgentToolInjector}'s injection rules. + * + *

The injector decides whether the OCG sub-agent gets attached as an + * {@code agent_tool} on every compiled top-level agent. Each rule below is + * a load-bearing safety property — if any of them slips, the main agent + * either misses OCG entirely (the user-visible bug) or recurses into + * itself / picks up duplicate tools (silent state corruption).

+ */ +class OcgAgentToolInjectorTest { + + @Test + void appendsOcgAgentToolAsLastEntryWhenEnabled() { + AgentConfig cfg = AgentConfig.builder() + .name("user_agent") + .tools(new ArrayList<>(List.of(workerTool("search")))) + .build(); + + AgentConfig out = OcgAgentToolInjector.inject(cfg, /*ocgEnabled=*/ true); + + assertThat(out.getTools()).hasSize(2); + ToolConfig injected = out.getTools().get(1); + assertThat(injected.getName()).isEqualTo("ocg_agent"); + assertThat(injected.getToolType()).isEqualTo("agent_tool"); + // workflowName must point at the registered _ocg_agent — otherwise + // ToolCompiler will fabricate ``ocg_agent_agent_wf`` and the + // SUB_WORKFLOW dispatch hits a missing workflow at runtime. + assertThat(injected.getConfig()).containsEntry("workflowName", "_ocg_agent"); + } + + @Test + void skipsInjectionWhenOcgDisabled() { + AgentConfig cfg = AgentConfig.builder() + .name("user_agent") + .tools(new ArrayList<>(List.of(workerTool("search")))) + .build(); + + AgentConfig out = OcgAgentToolInjector.inject(cfg, /*ocgEnabled=*/ false); + + assertThat(out.getTools()).hasSize(1); + assertThat(out.getTools().get(0).getName()).isEqualTo("search"); + } + + @Test + void doesNotSelfInjectIntoTheOcgAgentItself() { + // Without this guard the OCG sub-agent would gain itself as a tool + // and the LLM could recurse: ocg_agent → ocg_agent → ocg_agent → … + AgentConfig cfg = AgentConfig.builder() + .name(OcgAgentFactory.AGENT_NAME) + .tools(new ArrayList<>(OcgAgentFactory.buildTools())) + .build(); + + int before = cfg.getTools().size(); + AgentConfig out = OcgAgentToolInjector.inject(cfg, /*ocgEnabled=*/ true); + + assertThat(out.getTools()).hasSize(before); + assertThat(out.getTools().stream().map(ToolConfig::getName)).doesNotContain("ocg_agent"); + } + + @Test + void doesNotDuplicateWhenOcgAgentToolAlreadyPresent() { + // If a user (or a re-entrant compile) has already added an + // ocg_agent entry, a second pass must not silently append a + // duplicate — both would map to the same workflowName and confuse + // the LLM's tool spec list. + ToolConfig existing = ToolConfig.builder() + .name("ocg_agent") + .toolType("agent_tool") + .description("user-provided") + .build(); + AgentConfig cfg = AgentConfig.builder() + .name("user_agent") + .tools(new ArrayList<>(List.of(existing))) + .build(); + + AgentConfig out = OcgAgentToolInjector.inject(cfg, /*ocgEnabled=*/ true); + + assertThat(out.getTools()).hasSize(1); + assertThat(out.getTools().get(0).getDescription()).isEqualTo("user-provided"); + } + + @Test + void handlesNullToolsListByCreatingOne() { + AgentConfig cfg = AgentConfig.builder().name("user_agent").tools(null).build(); + + AgentConfig out = OcgAgentToolInjector.inject(cfg, /*ocgEnabled=*/ true); + + assertThat(out.getTools()).hasSize(1); + assertThat(out.getTools().get(0).getName()).isEqualTo("ocg_agent"); + } + + private static ToolConfig workerTool(String name) { + return ToolConfig.builder().name(name).toolType("worker").build(); + } +} diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java new file mode 100644 index 000000000..336a0e499 --- /dev/null +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.netflix.conductor.model.TaskModel; + +/** + * Unit tests for {@link OcgRequestTask}. + * + *

Each operation is exercised against a mocked {@link HttpClient} to pin the + * URL/method contract with the OCG service and the projection + capping + * behaviour.

+ */ +class OcgRequestTaskTest { + + private static OcgProperties props(String url) { + OcgProperties p = new OcgProperties(); + p.setUrl(url); + p.setResponseCapChars(8192); + return p; + } + + private static HttpResponse stub(int status, String body) { + @SuppressWarnings("unchecked") + HttpResponse resp = mock(HttpResponse.class); + when(resp.statusCode()).thenReturn(status); + when(resp.body()).thenReturn(body); + return resp; + } + + private static void stubSend(HttpClient http, HttpResponse response) throws Exception { + doReturn(response).when(http).send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)); + } + + private static TaskModel taskWith(Map input) { + TaskModel t = new TaskModel(); + t.setInputData(input); + return t; + } + + @Test + void queryOperationPostsToAgentQueryEndpoint() throws Exception { + HttpClient http = mock(HttpClient.class); + stubSend(http, stub(200, "{\"citations\":[{\"source_item_id\":\"a\",\"title\":\"t1\",\"snippet\":\"s\"}]}")); + OcgRequestTask task = new OcgRequestTask("OCG_QUERY", OcgRequestTask.OP_QUERY, props("http://ocg.local"), http); + + TaskModel t = taskWith(Map.of("query", "find foo", "max_results", 50)); + task.start(null, t, null); + + ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); + org.mockito.Mockito.verify(http).send(req.capture(), any()); + assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/agent/query"); + assertThat(req.getValue().method()).isEqualTo("POST"); + assertThat(t.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); + // Projection keeps citations[].source_item_id but JSON-serializes the + // whole result; the substring assertion pins both the projection and + // the serialized-into-``result`` shape downstream INLINEs read. + assertThat(t.getOutputData().get("result").toString()).contains("source_item_id"); + assertThat(t.getOutputData().get("operation")).isEqualTo("query"); + } + + @Test + void getEntityOperationGetsToEntitiesEndpoint() throws Exception { + HttpClient http = mock(HttpClient.class); + stubSend(http, stub(200, "{\"id\":\"e1\",\"type\":\"message\",\"title\":\"hello\"}")); + OcgRequestTask task = + new OcgRequestTask("OCG_GET_ENTITY", OcgRequestTask.OP_GET_ENTITY, props("http://ocg.local/"), http); + + TaskModel t = taskWith(Map.of("entity_id", "e1")); + task.start(null, t, null); + + ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); + org.mockito.Mockito.verify(http).send(req.capture(), any()); + // Trailing slash on the configured URL is trimmed. + assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/entities/e1"); + assertThat(req.getValue().method()).isEqualTo("GET"); + assertThat(t.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); + } + + @Test + void neighborhoodOperationIncludesDepthAndLimitQueryParams() throws Exception { + HttpClient http = mock(HttpClient.class); + stubSend(http, stub(200, "{\"center\":{\"id\":\"e1\"},\"edges\":[]}")); + OcgRequestTask task = + new OcgRequestTask("OCG_NEIGHBORHOOD", OcgRequestTask.OP_NEIGHBORHOOD, props("http://ocg.local"), http); + + TaskModel t = taskWith(Map.of("entity_id", "e1", "depth", 1, "limit", 5)); + task.start(null, t, null); + + ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); + org.mockito.Mockito.verify(http).send(req.capture(), any()); + assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/graph/neighborhood/e1?depth=1&limit=5"); + } + + @Test + void memoryDeleteOperationDispatchesDeleteWithQueryString() throws Exception { + HttpClient http = mock(HttpClient.class); + stubSend(http, stub(200, "{\"deleted\":true}")); + OcgRequestTask task = new OcgRequestTask( + "OCG_MEMORY_DELETE", OcgRequestTask.OP_MEMORY_DELETE, props("http://ocg.local"), http); + + TaskModel t = taskWith(Map.of("key", "k1", "agent", "agent:foo", "user", "user:bar")); + task.start(null, t, null); + + ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); + org.mockito.Mockito.verify(http).send(req.capture(), any()); + assertThat(req.getValue().method()).isEqualTo("DELETE"); + // URL-encoded query params; agent and user appear in declared order. + assertThat(req.getValue().uri().toString()) + .isEqualTo("http://ocg.local/memories/k1?agent=agent%3Afoo&user=user%3Abar"); + } + + @Test + void non2xxResponseSurfacesAsFailedStatus() throws Exception { + HttpClient http = mock(HttpClient.class); + stubSend(http, stub(503, "service unavailable")); + OcgRequestTask task = new OcgRequestTask("OCG_QUERY", OcgRequestTask.OP_QUERY, props("http://ocg.local"), http); + + TaskModel t = taskWith(Map.of("query", "x")); + task.start(null, t, null); + + assertThat(t.getStatus()).isEqualTo(TaskModel.Status.FAILED); + assertThat(t.getReasonForIncompletion()).contains("503"); + } + + @Test + void responseLargerThanCapIsTruncatedWithSuffix() throws Exception { + // Build a 50KB body so the post-projection JSON exceeds the 256-char + // cap configured below. The truncation suffix must be present and + // the result must not exceed the cap. + StringBuilder big = new StringBuilder("{\"citations\":["); + for (int i = 0; i < 200; i++) { + if (i > 0) big.append(','); + big.append("{\"source_item_id\":\"id") + .append(i) + .append("\",\"title\":\"title") + .append(i) + .append("\"}"); + } + big.append("]}"); + + HttpClient http = mock(HttpClient.class); + stubSend(http, stub(200, big.toString())); + OcgProperties p = props("http://ocg.local"); + p.setResponseCapChars(256); + OcgRequestTask task = new OcgRequestTask("OCG_QUERY", OcgRequestTask.OP_QUERY, p, http); + + TaskModel t = taskWith(Map.of("query", "x")); + task.start(null, t, null); + + String result = (String) t.getOutputData().get("result"); + assertThat(t.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); + assertThat(result).hasSizeLessThanOrEqualTo(256); + assertThat(result).endsWith("...[truncated]"); + } + + @Test + void disabledPropertiesYieldsFailedTask() throws Exception { + HttpClient http = mock(HttpClient.class); + OcgRequestTask task = new OcgRequestTask("OCG_QUERY", OcgRequestTask.OP_QUERY, props(""), http); + + TaskModel t = taskWith(Map.of("query", "x")); + task.start(null, t, null); + + assertThat(t.getStatus()).isEqualTo(TaskModel.Status.FAILED); + assertThat(t.getReasonForIncompletion()).contains("not configured"); + // Importantly: no HTTP call attempted when disabled. + org.mockito.Mockito.verifyNoInteractions(http); + } +} diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgToolCompilerIntegrationTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgToolCompilerIntegrationTest.java new file mode 100644 index 000000000..dc4826780 --- /dev/null +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgToolCompilerIntegrationTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import dev.agentspan.runtime.compiler.ToolCompiler; +import dev.agentspan.runtime.model.ToolConfig; + +/** + * Integration test pinning the OCG tool type → Conductor task type contract + * between {@link OcgAgentFactory} and {@link ToolCompiler}. + * + *

If anyone ever renames an OCG tool type without updating ToolCompiler's + * TYPE_MAP, the LLM tool call would fall through to a SIMPLE task with no + * worker and the workflow would hang forever. This test catches that drift + * at compile time on the test surface.

+ */ +class OcgToolCompilerIntegrationTest { + + @Test + void everyOcgToolSpecCompilesToItsRegisteredSystemTaskType() { + ToolCompiler compiler = new ToolCompiler(); + List tools = OcgAgentFactory.buildTools(); + List> specs = compiler.compileToolSpecs(tools); + + Map expectedTaskTypes = Map.of( + "ocg_query", "OCG_QUERY", + "ocg_get_entity", "OCG_GET_ENTITY", + "ocg_neighborhood", "OCG_NEIGHBORHOOD", + "ocg_code_history", "OCG_CODE_HISTORY", + "ocg_memory_set", "OCG_MEMORY_SET", + "ocg_memory_reinforce", "OCG_MEMORY_REINFORCE", + "ocg_memory_delete", "OCG_MEMORY_DELETE"); + + for (Map spec : specs) { + String name = (String) spec.get("name"); + String conductorType = (String) spec.get("type"); + assertThat(conductorType) + .as("OCG tool '%s' must compile to its registered task type", name) + .isEqualTo(expectedTaskTypes.get(name)); + } + } +} diff --git a/server/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java b/server/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java index 1ebaa7b4c..047316224 100644 --- a/server/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java +++ b/server/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java @@ -44,8 +44,8 @@ void tearDown() { private List> enrich(String knownNamesJson, String toolCallsJson) throws Exception { // All optional config maps are empty so every name falls through to the // generic SIMPLE-or-unknown branch. That's the path the harness uses. - String script = - JavaScriptBuilder.enrichToolsScript("{}", "{}", "{}", "{}", "{}", "{}", "{}", "{}", knownNamesJson); + String script = JavaScriptBuilder.enrichToolsScript( + "{}", "{}", "{}", "{}", "{}", "{}", "{}", "{}", "{}", knownNamesJson); // Wrap so the script's IIFE return is captured AND we get a JSON string // back — Graal's Value.toString() is JS source, not JSON. String wrapped = "var $ = {" From b1ac11f743d6674a20c09cf1a741bfe0ea1fc4e8 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Mon, 8 Jun 2026 14:47:27 -0700 Subject: [PATCH 04/61] feat(ocg): send bearer auth header on OCG requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires OCG_API_KEY env → agentspan.ocg.api-key → Authorization: Bearer header on every OCG_* system task's HTTP request. Empty key keeps the header off so unauthenticated local OCG instances still work. --- .../agentspan/runtime/ocg/OcgProperties.java | 11 ++++++ .../agentspan/runtime/ocg/OcgRequestTask.java | 3 ++ .../src/main/resources/application.properties | 1 + .../runtime/ocg/OcgRequestTaskTest.java | 35 +++++++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java index d4ce8bfd8..2e5d03be0 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java @@ -26,6 +26,13 @@ public class OcgProperties { /** Base URL of the OCG service. Empty / null disables the entire OCG feature. */ private String url; + /** + * Bearer token sent on every OCG HTTP request as + * {@code Authorization: Bearer }. Empty / null means no auth + * header — useful for local dev against an unauthenticated OCG instance. + */ + private String apiKey; + /** Model used by the OCG sub-agent's LLM turns. */ private String model = "openai/gpt-4o-mini"; @@ -39,4 +46,8 @@ public class OcgProperties { public boolean isEnabled() { return url != null && !url.isBlank(); } + + public boolean hasApiKey() { + return apiKey != null && !apiKey.isBlank(); + } } diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java index b417d89bc..e4bd35e53 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java @@ -131,6 +131,9 @@ public void start(WorkflowModel workflow, TaskModel task, WorkflowExecutor execu HttpRequest buildRequest(Map input) throws Exception { String baseUrl = trimTrailingSlash(properties.getUrl()); HttpRequest.Builder b = HttpRequest.newBuilder().timeout(READ_TIMEOUT).header("Accept", "application/json"); + if (properties.hasApiKey()) { + b.header("Authorization", "Bearer " + properties.getApiKey()); + } switch (operation) { case OP_QUERY -> { diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index abe167be5..653496895 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -183,6 +183,7 @@ agentspan.credentials.resolve.rate-limit=120 # - Every top-level agent is silently given an "ocg_agent" agent_tool, # so the main agent's LLM can delegate to OCG when it needs context. agentspan.ocg.url=${OCG_URL:} +agentspan.ocg.api-key=${OCG_API_KEY:} agentspan.ocg.model=${OCG_MODEL:openai/gpt-4o-mini} agentspan.ocg.response-cap-chars=8192 diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java index 336a0e499..5ac92be68 100644 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java @@ -171,6 +171,41 @@ void responseLargerThanCapIsTruncatedWithSuffix() throws Exception { assertThat(result).endsWith("...[truncated]"); } + @Test + void authorizationHeaderAttachedWhenApiKeySet() throws Exception { + HttpClient http = mock(HttpClient.class); + stubSend(http, stub(200, "{\"citations\":[]}")); + OcgProperties p = props("http://ocg.local"); + p.setApiKey("secret-key-123"); + OcgRequestTask task = new OcgRequestTask("OCG_QUERY", OcgRequestTask.OP_QUERY, p, http); + + TaskModel t = taskWith(Map.of("query", "x")); + task.start(null, t, null); + + ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); + org.mockito.Mockito.verify(http).send(req.capture(), any()); + // Bearer scheme — pinning both the header name and the prefix so a + // refactor can't silently change to e.g. ``X-API-Key`` without + // tripping a test. + assertThat(req.getValue().headers().firstValue("Authorization")).hasValue("Bearer secret-key-123"); + } + + @Test + void noAuthorizationHeaderWhenApiKeyUnset() throws Exception { + HttpClient http = mock(HttpClient.class); + stubSend(http, stub(200, "{\"citations\":[]}")); + // props(...) does not set an api key → header must be omitted so + // unauthenticated local OCG instances keep working. + OcgRequestTask task = new OcgRequestTask("OCG_QUERY", OcgRequestTask.OP_QUERY, props("http://ocg.local"), http); + + TaskModel t = taskWith(Map.of("query", "x")); + task.start(null, t, null); + + ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); + org.mockito.Mockito.verify(http).send(req.capture(), any()); + assertThat(req.getValue().headers().firstValue("Authorization")).isEmpty(); + } + @Test void disabledPropertiesYieldsFailedTask() throws Exception { HttpClient http = mock(HttpClient.class); From 6db7c32b3dc3c10da854185fe5f088f9fa72190a Mon Sep 17 00:00:00 2001 From: nicholascole Date: Mon, 8 Jun 2026 17:31:36 -0700 Subject: [PATCH 05/61] refactor(server): generic auto-expose mechanism + fix OCG dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two intertwined changes that ended up in one commit because they touch the same files: 1) Fix OCG end-to-end dispatch (was broken on the previous tip). - Register a TaskDef for each ocg_* tool name in OcgSubAgentService. Conductor resolves dynamic-fork tasks by name in the TaskDef registry; without the def, dispatch failed with "Cannot find task by name ocg_query in the task definitions". - Prefix every OCG endpoint with /api/v1 (was hitting /agent/query etc., the real paths are /api/v1/agent/query etc.). The OCG service returned grpc-gateway 404 NOT_FOUND on every call. With both fixes the full chain works: main LLM → ocg_agent SUB_WORKFLOW → ocg_query OCG_QUERY task → POST /api/v1/agent/query → real citations back. 2) Replace the OCG-specific Injector with a generic compiler hook. - AgentCompiler picks up any WorkflowDef whose metadata carries the agentspan.autoExposeAsTool flag and appends it as an agent_tool on every top-level compile. Self-recursion + duplicate guards live inside the merger. Optional @Autowired MetadataDAO keeps existing `new AgentCompiler()` test paths working unchanged. - OcgSubAgentService stamps the flag on _ocg_agent's metadata at startup. That's the *only* line that ties OCG to LLM visibility — everything else is generic. - OcgAgentToolInjector + its 5-test pinning class are deleted. AutoExposedToolsMergeTest (6 tests) replaces them and is deliberately not OCG-specific, so future server-side sub-agents rely on the same contract. - AgentService loses its @Autowired OcgSubAgentService field and the maybeInjectOcgAgentTool helper. resolveConfig is back to a plain normalize-or-passthrough. Future server-side sub-agents (`_foo_agent`, `_bar_agent`, …) now drop in by stamping the same metadata flag and registering their workflow. No AgentCompiler change, no AgentService change, no per-feature injection class. --- .../runtime/compiler/AgentCompiler.java | 112 +++++++++++ .../runtime/ocg/OcgAgentFactory.java | 19 ++ .../runtime/ocg/OcgAgentToolInjector.java | 78 ------- .../agentspan/runtime/ocg/OcgRequestTask.java | 20 +- .../runtime/ocg/OcgSubAgentService.java | 57 +++++- .../runtime/service/AgentService.java | 43 +--- .../compiler/AutoExposedToolsMergeTest.java | 190 ++++++++++++++++++ .../runtime/ocg/OcgAgentToolInjectorTest.java | 112 ----------- .../runtime/ocg/OcgRequestTaskTest.java | 9 +- 9 files changed, 397 insertions(+), 243 deletions(-) delete mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentToolInjector.java create mode 100644 server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java delete mode 100644 server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentToolInjectorTest.java diff --git a/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java b/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java index 91c86f722..91c26e34e 100644 --- a/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java +++ b/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java @@ -10,12 +10,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.conductor.common.metadata.workflow.SubWorkflowParams; import com.netflix.conductor.common.metadata.workflow.WorkflowDef; import com.netflix.conductor.common.metadata.workflow.WorkflowTask; +import com.netflix.conductor.dao.MetadataDAO; import dev.agentspan.runtime.model.*; import dev.agentspan.runtime.util.JavaScriptBuilder; @@ -39,11 +41,34 @@ public class AgentCompiler { "message", "${workflow.input.prompt}", "media", "${workflow.input.media}"); + /** + * Metadata key on a {@link WorkflowDef} that marks the workflow as one + * that should be silently appended to every top-level agent's tool list + * at compile time. The value is a {@code Map} with at + * least {@code name} and {@code description}; both are surfaced to the + * agent's LLM via the {@code agent_tool} routing. + * + *

This is the single registration mechanism for "server-side + * capability the user's LLM can call." A new sub-agent only has to: + * (a) compile and register its WorkflowDef via {@link MetadataDAO}, + * (b) stamp this metadata key. {@link #compile} picks it up generically + * — no per-feature injection class required.

+ */ + public static final String AUTO_EXPOSE_AS_TOOL_METADATA_KEY = "agentspan.autoExposeAsTool"; + private int timeoutSeconds = 0; private int llmRetryCount = 3; private int contextMaxSizeBytes = 32768; private int contextMaxValueSizeBytes = 4096; + /** + * Optional — only injected at runtime. Tests that construct + * {@code new AgentCompiler()} directly leave this null, and the + * auto-exposed-tool merge step becomes a no-op for them. + */ + @Autowired(required = false) + private MetadataDAO metadataDAO; + /** * Sanitizes an agent name for use as a Conductor task reference name. * @@ -90,8 +115,19 @@ String getText() { /** * Main entry point: compile an AgentConfig into a WorkflowDef. + * + *

Before strategy dispatch, any workflow registered in the metadata + * store with the {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} flag is + * silently appended to {@code config.tools} as an {@code agent_tool}. + * That's how the OCG sub-agent (and any future server-side sub-agent) + * becomes LLM-visible without per-feature injection code. Only the + * top-level compile runs this merge — nested sub-agents go through + * {@link #compileSubAgent}, which deliberately skips it so a specialist + * sub-agent isn't polluted with an unrelated retrieval tool.

*/ public WorkflowDef compile(AgentConfig config) { + mergeAutoExposedTools(config); + WorkflowDef wf; // Passthrough check MUST be first — passthrough configs have null model. @@ -2426,6 +2462,82 @@ static Set collectCapabilities(AgentConfig config) { return caps; } + /** + * Append every {@link MetadataDAO}-registered workflow that carries the + * {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} marker to {@code config.tools} + * as an {@code agent_tool}. + * + *

Mutates {@code config} in place. Skips when:

+ *
    + *
  • {@link #metadataDAO} is absent (unit-test path)
  • + *
  • The workflow being compiled IS the auto-exposed one + * — no self-recursion
  • + *
  • A tool with that name is already declared on the config + * — caller's explicit declaration wins
  • + *
+ */ + @SuppressWarnings("unchecked") + void mergeAutoExposedTools(AgentConfig config) { + if (config == null || metadataDAO == null) return; + List defs; + try { + defs = metadataDAO.getAllWorkflowDefsLatestVersions(); + } catch (Exception e) { + // Defensive — a transient DAO failure shouldn't fail the compile; + // the merge is a convenience layer, not a correctness requirement. + log.warn("auto-expose merge: metadataDAO lookup failed, skipping. {}", e.getMessage()); + return; + } + if (defs == null || defs.isEmpty()) return; + + List tools = config.getTools(); + Set existingNames = new HashSet<>(); + if (tools != null) { + for (ToolConfig t : tools) { + if (t.getName() != null) existingNames.add(t.getName()); + } + } + + List merged = null; + for (WorkflowDef def : defs) { + Map md = def.getMetadata(); + if (md == null) continue; + Object spec = md.get(AUTO_EXPOSE_AS_TOOL_METADATA_KEY); + if (!(spec instanceof Map specMap)) continue; + Object nameObj = specMap.get("name"); + if (!(nameObj instanceof String toolName) || toolName.isEmpty()) continue; + + // Self-recursion guard: the workflow being compiled cannot have + // itself appended as a tool. Match on the workflow def's name + // (the Conductor registration name, e.g. "_ocg_agent"), since + // that's what config.getName() resolves to during the + // sub-agent's own compile. + if (def.getName().equals(config.getName())) continue; + + // Duplicate guard: skip names already present on the config. + if (existingNames.contains(toolName)) continue; + + Object descObj = specMap.get("description"); + String description = descObj instanceof String s ? s : ""; + + if (merged == null) { + merged = new ArrayList<>(tools != null ? tools : List.of()); + } + merged.add(ToolConfig.builder() + .name(toolName) + .toolType("agent_tool") + .description(description) + .config(Map.of("workflowName", def.getName())) + .build()); + existingNames.add(toolName); + log.info( + "Auto-exposed workflow '{}' as agent_tool '{}' on '{}'", def.getName(), toolName, config.getName()); + } + if (merged != null) { + config.setTools(merged); + } + } + // Setters for configuration public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java index 31159f393..99f8659c6 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java @@ -24,6 +24,25 @@ public final class OcgAgentFactory { /** Stable workflow name for the registered OCG sub-agent. */ public static final String AGENT_NAME = "_ocg_agent"; + /** + * Tool name as the main agent's LLM sees it. Distinct from {@link #AGENT_NAME} + * because workflows registered server-side use the underscore-prefix + * convention while LLM-visible tool names don't. + */ + public static final String TOOL_NAME = "ocg_agent"; + + /** + * Description shown to the main agent's LLM in its tool spec list. Kept + * concrete enough that the model can decide when to delegate + * without leaking implementation details into the user-facing prompt. + */ + public static final String TOOL_DESCRIPTION = + "Delegate to the OCG (Open Context Graph) retrieval agent when you need " + + "context from the knowledge graph — message search, entity lookup, " + + "code history, or stored memories. Provide a focused natural-language " + + "query (under ~15 content words). Returns a synthesized answer with " + + "supporting citations."; + /** * System prompt for the OCG sub-agent. Supplied verbatim by the feature * spec — explains the retrieval/aggregation split and the two-step diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentToolInjector.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentToolInjector.java deleted file mode 100644 index c56f9bec1..000000000 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentToolInjector.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import dev.agentspan.runtime.model.AgentConfig; -import dev.agentspan.runtime.model.ToolConfig; - -/** - * Silently appends the OCG sub-agent as an {@code agent_tool} on the top-level - * {@link AgentConfig} when OCG is enabled. - * - *

Extracted from {@code AgentService} as a static helper so the injection - * rules (skip self, skip duplicates, never touch nested sub-agents) are - * unit-testable without standing up the full service.

- */ -public final class OcgAgentToolInjector { - - public static final String OCG_AGENT_TOOL_NAME = "ocg_agent"; - - /** - * Description shown to the main agent's LLM in its tool spec list. Keep - * it concrete enough that the model can decide *when* to delegate, but - * short enough not to waste tokens. - */ - static final String OCG_AGENT_TOOL_DESCRIPTION = - "Delegate to the OCG (Open Context Graph) retrieval agent when you need " - + "context from the knowledge graph — message search, entity lookup, " - + "code history, or stored memories. Provide a focused natural-language " - + "query (under ~15 content words). Returns a synthesized answer with " - + "supporting citations."; - - private OcgAgentToolInjector() {} - - /** - * Returns {@code config} with an {@code ocg_agent} tool appended if and - * only if all of the following hold: - *
    - *
  • {@code ocgEnabled} is true
  • - *
  • {@code config} is not the registered {@code _ocg_agent} itself (no self-recursion)
  • - *
  • {@code config} doesn't already declare a tool named {@code ocg_agent}
  • - *
- * Otherwise returns {@code config} unchanged. - * - *

Nested sub-agents are intentionally untouched — only the top-level - * agent receives the injection, so a specialist sub-agent isn't polluted - * with an unrelated retrieval tool.

- */ - public static AgentConfig inject(AgentConfig config, boolean ocgEnabled) { - if (config == null || !ocgEnabled) return config; - if (OcgAgentFactory.AGENT_NAME.equals(config.getName())) return config; - - List existing = config.getTools(); - if (existing != null) { - for (ToolConfig t : existing) { - if (OCG_AGENT_TOOL_NAME.equals(t.getName())) { - return config; - } - } - } - List merged = new ArrayList<>(); - if (existing != null) merged.addAll(existing); - merged.add(ToolConfig.builder() - .name(OCG_AGENT_TOOL_NAME) - .toolType("agent_tool") - .description(OCG_AGENT_TOOL_DESCRIPTION) - .config(Map.of("workflowName", OcgAgentFactory.AGENT_NAME)) - .build()); - config.setTools(merged); - return config; - } -} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java index e4bd35e53..da6e0359f 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java @@ -66,6 +66,12 @@ public class OcgRequestTask extends WorkflowSystemTask { private static final Duration READ_TIMEOUT = Duration.ofSeconds(30); private static final String TRUNCATE_SUFFIX = "...[truncated]"; + /** + * Every OCG endpoint sits under {@code /api/v1}. Keeping it as a single + * constant rather than inline so a future version bump is one-line. + */ + private static final String API_PREFIX = "/api/v1"; + private final String operation; private final OcgProperties properties; private final HttpClient httpClient; @@ -143,21 +149,21 @@ HttpRequest buildRequest(Map input) throws Exception { copyIfPresent(input, body, "traversal_level"); copyIfPresent(input, body, "start_time"); copyIfPresent(input, body, "end_time"); - return b.uri(URI.create(baseUrl + "/agent/query")) + return b.uri(URI.create(baseUrl + API_PREFIX + "/agent/query")) .header("Content-Type", "application/json") .POST(BodyPublishers.ofString(MAPPER.writeValueAsString(body), StandardCharsets.UTF_8)) .build(); } case OP_GET_ENTITY -> { String entityId = requiredString(input, "entity_id"); - return b.uri(URI.create(baseUrl + "/entities/" + urlEncode(entityId))) + return b.uri(URI.create(baseUrl + API_PREFIX + "/entities/" + urlEncode(entityId))) .GET() .build(); } case OP_NEIGHBORHOOD -> { String entityId = requiredString(input, "entity_id"); String query = "?depth=" + intOr(input.get("depth"), 2) + "&limit=" + intOr(input.get("limit"), 50); - return b.uri(URI.create(baseUrl + "/graph/neighborhood/" + urlEncode(entityId) + query)) + return b.uri(URI.create(baseUrl + API_PREFIX + "/graph/neighborhood/" + urlEncode(entityId) + query)) .GET() .build(); } @@ -165,14 +171,14 @@ HttpRequest buildRequest(Map input) throws Exception { String repoId = requiredString(input, "repo_id"); String path = requiredString(input, "path"); String query = "?path=" + urlEncode(path) + "&limit=" + intOr(input.get("limit"), 20); - return b.uri(URI.create(baseUrl + "/code/history/" + urlEncode(repoId) + query)) + return b.uri(URI.create(baseUrl + API_PREFIX + "/code/history/" + urlEncode(repoId) + query)) .GET() .build(); } case OP_MEMORY_SET -> { Map body = new LinkedHashMap<>(input); body.remove("__agentspan_ctx__"); - return b.uri(URI.create(baseUrl + "/memories")) + return b.uri(URI.create(baseUrl + API_PREFIX + "/memories")) .header("Content-Type", "application/json") .POST(BodyPublishers.ofString(MAPPER.writeValueAsString(body), StandardCharsets.UTF_8)) .build(); @@ -184,7 +190,7 @@ HttpRequest buildRequest(Map input) throws Exception { copyIfPresent(input, body, "user"); copyIfPresent(input, body, "confidence_boost"); copyIfPresent(input, body, "source_ref"); - return b.uri(URI.create(baseUrl + "/memories/" + urlEncode(key) + "/reinforce")) + return b.uri(URI.create(baseUrl + API_PREFIX + "/memories/" + urlEncode(key) + "/reinforce")) .header("Content-Type", "application/json") .POST(BodyPublishers.ofString(MAPPER.writeValueAsString(body), StandardCharsets.UTF_8)) .build(); @@ -200,7 +206,7 @@ HttpRequest buildRequest(Map input) throws Exception { q.append("user=").append(urlEncode(user)); } String suffix = q.length() > 0 ? "?" + q : ""; - return b.uri(URI.create(baseUrl + "/memories/" + urlEncode(key) + suffix)) + return b.uri(URI.create(baseUrl + API_PREFIX + "/memories/" + urlEncode(key) + suffix)) .DELETE() .build(); } diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgSubAgentService.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgSubAgentService.java index 628232f06..517773dce 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgSubAgentService.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgSubAgentService.java @@ -5,6 +5,10 @@ package dev.agentspan.runtime.ocg; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + import jakarta.annotation.PostConstruct; import org.slf4j.Logger; @@ -12,6 +16,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; +import com.netflix.conductor.common.metadata.tasks.TaskDef; import com.netflix.conductor.common.metadata.workflow.WorkflowDef; import com.netflix.conductor.dao.MetadataDAO; @@ -53,16 +58,66 @@ public void registerWorkflow() { // service directly with a disabled config without crashing. return; } + registerOcgTaskDefs(); AgentConfig config = OcgAgentFactory.build(properties); WorkflowDef def = agentCompiler.compile(config); + + // Stamp the auto-expose marker so AgentCompiler.mergeAutoExposedTools + // appends this workflow as an `ocg_agent` agent_tool on every + // subsequent top-level compile. This is the only line that ties OCG + // to LLM visibility — the rest is generic compiler machinery. + Map metadata = + def.getMetadata() != null ? new LinkedHashMap<>(def.getMetadata()) : new LinkedHashMap<>(); + metadata.put( + AgentCompiler.AUTO_EXPOSE_AS_TOOL_METADATA_KEY, + Map.of("name", OcgAgentFactory.TOOL_NAME, "description", OcgAgentFactory.TOOL_DESCRIPTION)); + def.setMetadata(metadata); + metadataDAO.updateWorkflowDef(def); log.info( - "OCG sub-agent registered: workflow='{}' model='{}' url='{}'", + "OCG sub-agent registered: workflow='{}' tool='{}' model='{}' url='{}'", def.getName(), + OcgAgentFactory.TOOL_NAME, properties.getModel(), properties.getUrl()); } + /** + * Register a {@link TaskDef} for each OCG tool name so Conductor's dynamic + * dispatch can resolve the task at runtime. + * + *

The enrichment script (see {@code JavaScriptBuilder.enrichToolsScript}) + * emits dynamic tasks with {@code name=} and + * {@code type=OCG_*}. Conductor looks up the task by name in the + * TaskDef registry before dispatching — without a matching def, the + * SUB_WORKFLOW fails with {@code "Cannot find task by name ocg_query in + * the task definitions"} and the parent LLM loop sees an opaque error.

+ * + *

{@code retryCount=0} on purpose: each OCG call is a stateless HTTP + * round-trip handled by {@link OcgRequestTask}; retries here would double + * the load and bypass the parent LLM's ability to refine the query.

+ */ + private void registerOcgTaskDefs() { + List names = List.of( + "ocg_query", + "ocg_get_entity", + "ocg_neighborhood", + "ocg_code_history", + "ocg_memory_set", + "ocg_memory_reinforce", + "ocg_memory_delete"); + for (String name : names) { + TaskDef def = new TaskDef(); + def.setName(name); + def.setRetryCount(0); + def.setTimeoutSeconds(60); + def.setResponseTimeoutSeconds(60); + def.setOwnerEmail("ocg@agentspan.dev"); + metadataDAO.updateTaskDef(def); + } + log.info("OCG TaskDefs registered: {}", names); + } + public boolean isEnabled() { return properties.isEnabled(); } diff --git a/server/src/main/java/dev/agentspan/runtime/service/AgentService.java b/server/src/main/java/dev/agentspan/runtime/service/AgentService.java index 2867d6a62..26f822f6e 100644 --- a/server/src/main/java/dev/agentspan/runtime/service/AgentService.java +++ b/server/src/main/java/dev/agentspan/runtime/service/AgentService.java @@ -47,9 +47,6 @@ import dev.agentspan.runtime.credentials.ExecutionTokenService; import dev.agentspan.runtime.model.*; import dev.agentspan.runtime.normalizer.NormalizerRegistry; -import dev.agentspan.runtime.ocg.OcgAgentFactory; -import dev.agentspan.runtime.ocg.OcgAgentToolInjector; -import dev.agentspan.runtime.ocg.OcgSubAgentService; import dev.agentspan.runtime.util.ModelParser; import dev.agentspan.runtime.util.ProviderValidator; @@ -78,9 +75,6 @@ public class AgentService { @Autowired(required = false) private SkillRegistryService skillRegistryService; - @Autowired(required = false) - private OcgSubAgentService ocgSubAgentService; - /** Package-private constructor for testing with ExecutionTokenService */ AgentService( AgentCompiler agentCompiler, @@ -1167,7 +1161,6 @@ private Optional validateModelProvider(AgentConfig config) { * Otherwise, use the native {@code agentConfig} field directly. */ private AgentConfig resolveConfig(StartRequest request) { - AgentConfig config; if (request.getFramework() != null && !request.getFramework().isEmpty()) { log.info("Normalizing framework '{}' agent config", request.getFramework()); if ("skill".equals(request.getFramework()) @@ -1178,41 +1171,9 @@ private AgentConfig resolveConfig(StartRequest request) { } request.setRawConfig(skillRegistryService.resolveRawConfig(request.getSkillRef())); } - config = normalizerRegistry.normalize(request.getFramework(), request.getRawConfig()); - } else { - config = request.getAgentConfig(); - } - return maybeInjectOcgAgentTool(config); - } - - /** - * Silently append the OCG sub-agent as an {@code agent_tool} on the - * top-level config when OCG is enabled. Pre-flight has been removed — - * the main agent's LLM decides whether to call OCG. - * - *

Logic lives in {@link OcgAgentToolInjector} so the rules (skip - * self, skip duplicates, only top-level) are unit-testable without the - * full service stack.

- */ - private AgentConfig maybeInjectOcgAgentTool(AgentConfig config) { - boolean enabled = ocgSubAgentService != null && ocgSubAgentService.isEnabled(); - AgentConfig result = OcgAgentToolInjector.inject(config, enabled); - if (enabled - && result != null - && result.getTools() != null - && !result.getTools().isEmpty() - && OcgAgentToolInjector.OCG_AGENT_TOOL_NAME.equals( - result.getTools().get(result.getTools().size() - 1).getName()) - && !OcgAgentFactory.AGENT_NAME.equals(result.getName())) { - // Logged only when injection actually happened on this call (last - // tool == ocg_agent and we aren't compiling the OCG agent itself). - // Duplicate-skip and self-skip paths stay silent. - log.info( - "Auto-injected '{}' agent_tool into '{}'", - OcgAgentToolInjector.OCG_AGENT_TOOL_NAME, - config != null ? config.getName() : ""); + return normalizerRegistry.normalize(request.getFramework(), request.getRawConfig()); } - return result; + return request.getAgentConfig(); } // ── SSE Streaming ────────────────────────────────────────────── diff --git a/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java b/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java new file mode 100644 index 000000000..2d4a7b721 --- /dev/null +++ b/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.compiler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import com.netflix.conductor.common.metadata.workflow.WorkflowDef; +import com.netflix.conductor.dao.MetadataDAO; + +import dev.agentspan.runtime.model.AgentConfig; +import dev.agentspan.runtime.model.ToolConfig; + +/** + * Unit tests for {@link AgentCompiler#mergeAutoExposedTools}. + * + *

Pins the generic auto-expose mechanism that lets any server-side + * sub-agent become LLM-visible to every top-level agent just by stamping + * a metadata flag on its {@link WorkflowDef}. These tests are intentionally + * not OCG-specific — OCG is one consumer; the contract here is the + * one every future consumer relies on.

+ */ +class AutoExposedToolsMergeTest { + + private AgentCompiler compiler; + private MetadataDAO metadataDAO; + + @BeforeEach + void setUp() { + compiler = new AgentCompiler(); + metadataDAO = mock(MetadataDAO.class); + ReflectionTestUtils.setField(compiler, "metadataDAO", metadataDAO); + } + + @Test + void appendsFlaggedWorkflowAsAgentTool() { + WorkflowDef flagged = wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "Use this when you need help."); + when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(flagged)); + + AgentConfig config = AgentConfig.builder() + .name("user_agent") + .tools(new ArrayList<>(List.of(workerTool("search")))) + .build(); + + compiler.mergeAutoExposedTools(config); + + assertThat(config.getTools()).hasSize(2); + ToolConfig injected = config.getTools().get(1); + assertThat(injected.getName()).isEqualTo("helper_agent"); + assertThat(injected.getToolType()).isEqualTo("agent_tool"); + // workflowName must match the registered WorkflowDef so SUB_WORKFLOW + // dispatch at runtime resolves to the right workflow — drift here + // would silently route to a missing workflow. + assertThat(injected.getConfig()).containsEntry("workflowName", "_helper_agent"); + assertThat(injected.getDescription()).isEqualTo("Use this when you need help."); + } + + @Test + void skipsWorkflowsWithoutTheMetadataKey() { + WorkflowDef plain = new WorkflowDef(); + plain.setName("_unrelated_workflow"); + plain.setMetadata(Map.of("some_other_key", "value")); + when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(plain)); + + AgentConfig config = AgentConfig.builder() + .name("user_agent") + .tools(new ArrayList<>(List.of(workerTool("search")))) + .build(); + + compiler.mergeAutoExposedTools(config); + + // Only the user-declared tool remains; the unflagged workflow is + // invisible to the merger. + assertThat(config.getTools()).hasSize(1); + assertThat(config.getTools().get(0).getName()).isEqualTo("search"); + } + + @Test + void doesNotInjectIntoTheAutoExposedWorkflowItself() { + // Self-recursion guard: if the compile target IS the flagged + // workflow, the merger must skip it. Without this, the OCG agent's + // own compile would see itself in the DAO and recursively gain + // itself as a tool — broken tool list + infinite delegation. + WorkflowDef flagged = wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "irrelevant"); + when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(flagged)); + + AgentConfig config = AgentConfig.builder() + .name("_helper_agent") + .tools(new ArrayList<>(List.of(workerTool("internal_tool")))) + .build(); + + compiler.mergeAutoExposedTools(config); + + assertThat(config.getTools()).hasSize(1); + assertThat(config.getTools().get(0).getName()).isEqualTo("internal_tool"); + } + + @Test + void doesNotDuplicateWhenAToolWithThatNameAlreadyExists() { + // The caller's explicit declaration wins. Two entries with the + // same name would confuse the LLM's tool spec list and cause both + // dispatches to resolve to the same workflow. + WorkflowDef flagged = wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "auto description"); + when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(flagged)); + + ToolConfig existing = ToolConfig.builder() + .name("helper_agent") + .toolType("agent_tool") + .description("user-provided description") + .build(); + AgentConfig config = AgentConfig.builder() + .name("user_agent") + .tools(new ArrayList<>(List.of(existing))) + .build(); + + compiler.mergeAutoExposedTools(config); + + assertThat(config.getTools()).hasSize(1); + assertThat(config.getTools().get(0).getDescription()).isEqualTo("user-provided description"); + } + + @Test + void noOpWhenMetadataDaoIsAbsent() { + // Tests that construct AgentCompiler with `new AgentCompiler()` + // (i.e. no Spring DI) must continue to work. The merger short- + // circuits cleanly when metadataDAO is null instead of NPE-ing. + AgentCompiler noDao = new AgentCompiler(); // no setField — metadataDAO stays null + + AgentConfig config = AgentConfig.builder() + .name("user_agent") + .tools(new ArrayList<>(List.of(workerTool("search")))) + .build(); + + noDao.mergeAutoExposedTools(config); + + assertThat(config.getTools()).hasSize(1); + } + + @Test + void appendsMultipleFlaggedWorkflowsInDaoOrder() { + WorkflowDef a = wfWithAutoExposeMetadata("_a_agent", "alpha_agent", "first"); + WorkflowDef b = wfWithAutoExposeMetadata("_b_agent", "beta_agent", "second"); + when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(a, b)); + + AgentConfig config = AgentConfig.builder() + .name("user_agent") + .tools(new ArrayList<>()) + .build(); + + compiler.mergeAutoExposedTools(config); + + // Both flagged workflows surface as agent_tools on the same config. + // This is the path that lets future server-side capabilities + // accumulate without any per-feature wiring. + assertThat(config.getTools()).hasSize(2); + assertThat(config.getTools().get(0).getName()).isEqualTo("alpha_agent"); + assertThat(config.getTools().get(1).getName()).isEqualTo("beta_agent"); + } + + // ───────────────────────────────────────────────────────────────────── + // Helpers + // ───────────────────────────────────────────────────────────────────── + + private static WorkflowDef wfWithAutoExposeMetadata(String workflowName, String toolName, String description) { + WorkflowDef def = new WorkflowDef(); + def.setName(workflowName); + Map metadata = new LinkedHashMap<>(); + metadata.put( + AgentCompiler.AUTO_EXPOSE_AS_TOOL_METADATA_KEY, Map.of("name", toolName, "description", description)); + def.setMetadata(metadata); + return def; + } + + private static ToolConfig workerTool(String name) { + return ToolConfig.builder().name(name).toolType("worker").build(); + } +} diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentToolInjectorTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentToolInjectorTest.java deleted file mode 100644 index dff7336e8..000000000 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentToolInjectorTest.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.ArrayList; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import dev.agentspan.runtime.model.AgentConfig; -import dev.agentspan.runtime.model.ToolConfig; - -/** - * Unit tests pinning {@link OcgAgentToolInjector}'s injection rules. - * - *

The injector decides whether the OCG sub-agent gets attached as an - * {@code agent_tool} on every compiled top-level agent. Each rule below is - * a load-bearing safety property — if any of them slips, the main agent - * either misses OCG entirely (the user-visible bug) or recurses into - * itself / picks up duplicate tools (silent state corruption).

- */ -class OcgAgentToolInjectorTest { - - @Test - void appendsOcgAgentToolAsLastEntryWhenEnabled() { - AgentConfig cfg = AgentConfig.builder() - .name("user_agent") - .tools(new ArrayList<>(List.of(workerTool("search")))) - .build(); - - AgentConfig out = OcgAgentToolInjector.inject(cfg, /*ocgEnabled=*/ true); - - assertThat(out.getTools()).hasSize(2); - ToolConfig injected = out.getTools().get(1); - assertThat(injected.getName()).isEqualTo("ocg_agent"); - assertThat(injected.getToolType()).isEqualTo("agent_tool"); - // workflowName must point at the registered _ocg_agent — otherwise - // ToolCompiler will fabricate ``ocg_agent_agent_wf`` and the - // SUB_WORKFLOW dispatch hits a missing workflow at runtime. - assertThat(injected.getConfig()).containsEntry("workflowName", "_ocg_agent"); - } - - @Test - void skipsInjectionWhenOcgDisabled() { - AgentConfig cfg = AgentConfig.builder() - .name("user_agent") - .tools(new ArrayList<>(List.of(workerTool("search")))) - .build(); - - AgentConfig out = OcgAgentToolInjector.inject(cfg, /*ocgEnabled=*/ false); - - assertThat(out.getTools()).hasSize(1); - assertThat(out.getTools().get(0).getName()).isEqualTo("search"); - } - - @Test - void doesNotSelfInjectIntoTheOcgAgentItself() { - // Without this guard the OCG sub-agent would gain itself as a tool - // and the LLM could recurse: ocg_agent → ocg_agent → ocg_agent → … - AgentConfig cfg = AgentConfig.builder() - .name(OcgAgentFactory.AGENT_NAME) - .tools(new ArrayList<>(OcgAgentFactory.buildTools())) - .build(); - - int before = cfg.getTools().size(); - AgentConfig out = OcgAgentToolInjector.inject(cfg, /*ocgEnabled=*/ true); - - assertThat(out.getTools()).hasSize(before); - assertThat(out.getTools().stream().map(ToolConfig::getName)).doesNotContain("ocg_agent"); - } - - @Test - void doesNotDuplicateWhenOcgAgentToolAlreadyPresent() { - // If a user (or a re-entrant compile) has already added an - // ocg_agent entry, a second pass must not silently append a - // duplicate — both would map to the same workflowName and confuse - // the LLM's tool spec list. - ToolConfig existing = ToolConfig.builder() - .name("ocg_agent") - .toolType("agent_tool") - .description("user-provided") - .build(); - AgentConfig cfg = AgentConfig.builder() - .name("user_agent") - .tools(new ArrayList<>(List.of(existing))) - .build(); - - AgentConfig out = OcgAgentToolInjector.inject(cfg, /*ocgEnabled=*/ true); - - assertThat(out.getTools()).hasSize(1); - assertThat(out.getTools().get(0).getDescription()).isEqualTo("user-provided"); - } - - @Test - void handlesNullToolsListByCreatingOne() { - AgentConfig cfg = AgentConfig.builder().name("user_agent").tools(null).build(); - - AgentConfig out = OcgAgentToolInjector.inject(cfg, /*ocgEnabled=*/ true); - - assertThat(out.getTools()).hasSize(1); - assertThat(out.getTools().get(0).getName()).isEqualTo("ocg_agent"); - } - - private static ToolConfig workerTool(String name) { - return ToolConfig.builder().name(name).toolType("worker").build(); - } -} diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java index 5ac92be68..10e73d200 100644 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java @@ -66,7 +66,7 @@ void queryOperationPostsToAgentQueryEndpoint() throws Exception { ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); org.mockito.Mockito.verify(http).send(req.capture(), any()); - assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/agent/query"); + assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/api/v1/agent/query"); assertThat(req.getValue().method()).isEqualTo("POST"); assertThat(t.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); // Projection keeps citations[].source_item_id but JSON-serializes the @@ -89,7 +89,7 @@ void getEntityOperationGetsToEntitiesEndpoint() throws Exception { ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); org.mockito.Mockito.verify(http).send(req.capture(), any()); // Trailing slash on the configured URL is trimmed. - assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/entities/e1"); + assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/api/v1/entities/e1"); assertThat(req.getValue().method()).isEqualTo("GET"); assertThat(t.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); } @@ -106,7 +106,8 @@ void neighborhoodOperationIncludesDepthAndLimitQueryParams() throws Exception { ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); org.mockito.Mockito.verify(http).send(req.capture(), any()); - assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/graph/neighborhood/e1?depth=1&limit=5"); + assertThat(req.getValue().uri().toString()) + .isEqualTo("http://ocg.local/api/v1/graph/neighborhood/e1?depth=1&limit=5"); } @Test @@ -124,7 +125,7 @@ void memoryDeleteOperationDispatchesDeleteWithQueryString() throws Exception { assertThat(req.getValue().method()).isEqualTo("DELETE"); // URL-encoded query params; agent and user appear in declared order. assertThat(req.getValue().uri().toString()) - .isEqualTo("http://ocg.local/memories/k1?agent=agent%3Afoo&user=user%3Abar"); + .isEqualTo("http://ocg.local/api/v1/memories/k1?agent=agent%3Afoo&user=user%3Abar"); } @Test From 644f28e67ee50efd97c5147db4bb3458fe9a03a9 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Tue, 9 Jun 2026 10:12:19 -0700 Subject: [PATCH 06/61] refactor(ocg): strategy pattern + Apache Commons cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the switch-statement-heavy OcgRequestTask with a per-endpoint strategy. Each operation owns its URL/method/body/projection in its own class; the task itself is a thin orchestrator. - OcgOperation interface: taskType(), name(), build(), project() - Seven concrete operations under runtime/ocg/operation/ — one per endpoint (query, get_entity, neighborhood, code_history, memory_{set,reinforce,delete}) - Three shared utilities in the same sub-package: OcgInputs — pick/required/intOrDefault/parseJsonLenient/writeJson OcgUri — UriComponentsBuilder rooted at /api/v1 OcgRequest — HttpRequest factory: base() / postJson() / get() / delete() - Apache Commons replaces hand-rolled helpers: StringUtils.removeEnd → trim trailing slash StringUtils.abbreviate → response cap (with custom marker) and log-body truncation Validate.isTrue → required-input check NumberUtils.toInt → string-to-int fallback - Spring UriComponentsBuilder handles URL encoding properly. One behavioural improvement: query params no longer over-encode ':' to '%3A' (it's not reserved in RFC 3986 query values). Updated the memoryDelete test assertion accordingly. - OcgRequestTask is now ~100 lines total. Largest method is 16 lines; every other method is under 10. No switch statements anywhere. - OcgRequestTaskConfig becomes one @Bean per operation, each pairing a fresh OcgRequestTask with its strategy. End-to-end smoke test on the live OCG dev instance still works — same shape, same citations, same token count. --- .../agentspan/runtime/ocg/OcgRequestTask.java | 338 +++--------------- .../runtime/ocg/OcgRequestTaskConfig.java | 70 ++-- .../operation/OcgCodeHistoryOperation.java | 50 +++ .../ocg/operation/OcgGetEntityOperation.java | 45 +++ .../runtime/ocg/operation/OcgInputs.java | 94 +++++ .../operation/OcgMemoryDeleteOperation.java | 47 +++ .../OcgMemoryReinforceOperation.java | 40 +++ .../ocg/operation/OcgMemorySetOperation.java | 45 +++ .../operation/OcgNeighborhoodOperation.java | 50 +++ .../runtime/ocg/operation/OcgOperation.java | 45 +++ .../ocg/operation/OcgQueryOperation.java | 68 ++++ .../runtime/ocg/operation/OcgRequest.java | 62 ++++ .../runtime/ocg/operation/OcgUri.java | 39 ++ .../runtime/ocg/OcgRequestTaskTest.java | 57 +-- 14 files changed, 710 insertions(+), 340 deletions(-) create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java index da6e0359f..a198ebcee 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java @@ -5,95 +5,59 @@ package dev.agentspan.runtime.ocg; -import java.net.URI; -import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; -import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.util.ArrayList; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.Objects; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.conductor.core.execution.WorkflowExecutor; import com.netflix.conductor.core.execution.tasks.WorkflowSystemTask; import com.netflix.conductor.model.TaskModel; import com.netflix.conductor.model.WorkflowModel; +import dev.agentspan.runtime.ocg.operation.OcgInputs; +import dev.agentspan.runtime.ocg.operation.OcgOperation; + /** * System task that proxies a single OCG (Open Context Graph) operation. * - *

One {@link OcgRequestTask} instance is registered per OCG endpoint via - * {@link OcgRequestTaskConfig}. The task type strings (e.g. {@code OCG_QUERY}, - * {@code OCG_GET_ENTITY}) are stable contracts the OCG sub-agent's tool calls - * dispatch to.

- * - *

Each {@link #start} call:

- *
    - *
  1. Reads operation-specific arguments from {@code task.inputData}
  2. - *
  3. Issues an HTTP request to OCG (path + method determined by operation)
  4. - *
  5. Projects the response to the fields the LLM actually needs
  6. - *
  7. Caps the JSON-serialized projection to - * {@link OcgProperties#getResponseCapChars()} so a 5MB graph traversal - * can't blow the model's context window
  8. - *
- * - *

HTTP I/O uses the same {@link HttpClient} pattern as - * {@code PlannerContextFetchTask} — synchronous, with sensible timeouts, and - * a constructor-injection seam for unit testing without the network.

+ *

This class is the thin orchestrator: enabled-check → send → project → + * cap → COMPLETED/FAILED. The endpoint-specific work (URL, method, body, + * field projection) lives in the strategy passed via {@link OcgOperation}. + * One {@link OcgRequestTask} bean per operation is registered by + * {@link OcgRequestTaskConfig}; the bean name is the operation's task + * type, which Conductor's {@code SystemTaskRegistry} dispatches on.

*/ public class OcgRequestTask extends WorkflowSystemTask { - public static final String OP_QUERY = "query"; - public static final String OP_GET_ENTITY = "get_entity"; - public static final String OP_NEIGHBORHOOD = "neighborhood"; - public static final String OP_CODE_HISTORY = "code_history"; - public static final String OP_MEMORY_SET = "memory_set"; - public static final String OP_MEMORY_REINFORCE = "memory_reinforce"; - public static final String OP_MEMORY_DELETE = "memory_delete"; + private static final Logger log = LoggerFactory.getLogger(OcgRequestTask.class); - private static final Logger logger = LoggerFactory.getLogger(OcgRequestTask.class); - private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String TRUNCATE_MARKER = "...[truncated]"; + private static final int LOG_BODY_LIMIT = 256; private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10); - private static final Duration READ_TIMEOUT = Duration.ofSeconds(30); - private static final String TRUNCATE_SUFFIX = "...[truncated]"; - - /** - * Every OCG endpoint sits under {@code /api/v1}. Keeping it as a single - * constant rather than inline so a future version bump is one-line. - */ - private static final String API_PREFIX = "/api/v1"; - private final String operation; + private final OcgOperation operation; private final OcgProperties properties; private final HttpClient httpClient; - public OcgRequestTask(String taskType, String operation, OcgProperties properties) { - this( - taskType, - operation, - properties, - HttpClient.newBuilder() - .connectTimeout(CONNECT_TIMEOUT) - .followRedirects(HttpClient.Redirect.NORMAL) - .build()); + public OcgRequestTask(OcgOperation operation, OcgProperties properties) { + this(operation, properties, defaultHttpClient()); } /** Visible-for-testing constructor with an injectable {@link HttpClient}. */ - OcgRequestTask(String taskType, String operation, OcgProperties properties, HttpClient httpClient) { - super(taskType); - this.operation = Objects.requireNonNull(operation, "operation"); + OcgRequestTask(OcgOperation operation, OcgProperties properties, HttpClient httpClient) { + super(Objects.requireNonNull(operation, "operation").taskType()); + this.operation = operation; this.properties = Objects.requireNonNull(properties, "properties"); this.httpClient = Objects.requireNonNull(httpClient, "httpClient"); - logger.debug("OcgRequestTask registered (taskType={}, operation={})", taskType, operation); + log.debug("OcgRequestTask registered (taskType={}, operation={})", operation.taskType(), operation.name()); } @Override @@ -102,248 +66,54 @@ public void start(WorkflowModel workflow, TaskModel task, WorkflowExecutor execu fail(task, "OCG is not configured (agentspan.ocg.url is empty)"); return; } - - Map input = task.getInputData() == null ? Map.of() : task.getInputData(); - try { - HttpRequest request = buildRequest(input); - HttpResponse resp = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - int status = resp.statusCode(); - if (status < 200 || status >= 300) { - fail(task, "OCG " + operation + " returned status " + status + ": " + truncateForLog(resp.body())); + HttpResponse response = send(task); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + fail( + task, + "OCG " + operation.name() + " returned status " + response.statusCode() + ": " + + StringUtils.abbreviate(response.body(), LOG_BODY_LIMIT)); return; } - Object parsed = parseJsonLenient(resp.body()); - Object projected = project(parsed); - String serialized = MAPPER.writeValueAsString(projected); - if (serialized.length() > properties.getResponseCapChars()) { - int cap = Math.max(0, properties.getResponseCapChars() - TRUNCATE_SUFFIX.length()); - serialized = serialized.substring(0, cap) + TRUNCATE_SUFFIX; - } - Map output = new LinkedHashMap<>(); - output.put("result", serialized); - output.put("operation", operation); - task.setOutputData(output); - task.setStatus(TaskModel.Status.COMPLETED); + complete(task, response.body()); } catch (Exception e) { - fail(task, "OCG " + operation + " failed: " + e.getMessage()); - } - } - - // ───────────────────────────────────────────────────────────────────── - // Operation → HTTP request mapping - // ───────────────────────────────────────────────────────────────────── - - HttpRequest buildRequest(Map input) throws Exception { - String baseUrl = trimTrailingSlash(properties.getUrl()); - HttpRequest.Builder b = HttpRequest.newBuilder().timeout(READ_TIMEOUT).header("Accept", "application/json"); - if (properties.hasApiKey()) { - b.header("Authorization", "Bearer " + properties.getApiKey()); - } - - switch (operation) { - case OP_QUERY -> { - Map body = new LinkedHashMap<>(); - copyIfPresent(input, body, "query"); - copyIfPresent(input, body, "max_results"); - copyIfPresent(input, body, "traversal_level"); - copyIfPresent(input, body, "start_time"); - copyIfPresent(input, body, "end_time"); - return b.uri(URI.create(baseUrl + API_PREFIX + "/agent/query")) - .header("Content-Type", "application/json") - .POST(BodyPublishers.ofString(MAPPER.writeValueAsString(body), StandardCharsets.UTF_8)) - .build(); - } - case OP_GET_ENTITY -> { - String entityId = requiredString(input, "entity_id"); - return b.uri(URI.create(baseUrl + API_PREFIX + "/entities/" + urlEncode(entityId))) - .GET() - .build(); - } - case OP_NEIGHBORHOOD -> { - String entityId = requiredString(input, "entity_id"); - String query = "?depth=" + intOr(input.get("depth"), 2) + "&limit=" + intOr(input.get("limit"), 50); - return b.uri(URI.create(baseUrl + API_PREFIX + "/graph/neighborhood/" + urlEncode(entityId) + query)) - .GET() - .build(); - } - case OP_CODE_HISTORY -> { - String repoId = requiredString(input, "repo_id"); - String path = requiredString(input, "path"); - String query = "?path=" + urlEncode(path) + "&limit=" + intOr(input.get("limit"), 20); - return b.uri(URI.create(baseUrl + API_PREFIX + "/code/history/" + urlEncode(repoId) + query)) - .GET() - .build(); - } - case OP_MEMORY_SET -> { - Map body = new LinkedHashMap<>(input); - body.remove("__agentspan_ctx__"); - return b.uri(URI.create(baseUrl + API_PREFIX + "/memories")) - .header("Content-Type", "application/json") - .POST(BodyPublishers.ofString(MAPPER.writeValueAsString(body), StandardCharsets.UTF_8)) - .build(); - } - case OP_MEMORY_REINFORCE -> { - String key = requiredString(input, "key"); - Map body = new LinkedHashMap<>(); - copyIfPresent(input, body, "agent"); - copyIfPresent(input, body, "user"); - copyIfPresent(input, body, "confidence_boost"); - copyIfPresent(input, body, "source_ref"); - return b.uri(URI.create(baseUrl + API_PREFIX + "/memories/" + urlEncode(key) + "/reinforce")) - .header("Content-Type", "application/json") - .POST(BodyPublishers.ofString(MAPPER.writeValueAsString(body), StandardCharsets.UTF_8)) - .build(); - } - case OP_MEMORY_DELETE -> { - String key = requiredString(input, "key"); - String agent = stringOrEmpty(input.get("agent")); - String user = stringOrEmpty(input.get("user")); - StringBuilder q = new StringBuilder(); - if (!agent.isEmpty()) q.append("agent=").append(urlEncode(agent)); - if (!user.isEmpty()) { - if (q.length() > 0) q.append('&'); - q.append("user=").append(urlEncode(user)); - } - String suffix = q.length() > 0 ? "?" + q : ""; - return b.uri(URI.create(baseUrl + API_PREFIX + "/memories/" + urlEncode(key) + suffix)) - .DELETE() - .build(); - } - default -> throw new IllegalStateException("Unsupported OCG operation: " + operation); + fail(task, "OCG " + operation.name() + " failed: " + e.getMessage()); } } - // ───────────────────────────────────────────────────────────────────── - // Response projection (mirrors Python `_project_*` helpers) - // ───────────────────────────────────────────────────────────────────── - - @SuppressWarnings("unchecked") - Object project(Object raw) { - if (!(raw instanceof Map)) return raw; - Map map = (Map) raw; - return switch (operation) { - case OP_QUERY -> projectQuery(map); - case OP_GET_ENTITY -> projectEntity(map); - case OP_NEIGHBORHOOD -> projectNeighborhood(map); - case OP_CODE_HISTORY -> projectCodeHistory(map); - default -> map; // memory_* — pass through - }; - } - - @SuppressWarnings("unchecked") - private Map projectQuery(Map raw) { - Map out = new LinkedHashMap<>(); - Object citations = raw.get("citations"); - List> projectedCitations = new ArrayList<>(); - if (citations instanceof List list) { - for (Object item : list) { - if (item instanceof Map c) { - Map cit = (Map) c; - Map projected = new LinkedHashMap<>(); - copyIfPresent(cit, projected, "source_item_id"); - copyIfPresent(cit, projected, "title"); - copyIfPresent(cit, projected, "container_id"); - copyIfPresent(cit, projected, "snippet"); - projectedCitations.add(projected); - } - } - } - out.put("citations", projectedCitations); - if (raw.containsKey("traversal_results")) { - out.put("traversal_results", raw.get("traversal_results")); - } - return out; + private HttpResponse send(TaskModel task) throws Exception { + Map input = task.getInputData() != null ? task.getInputData() : Map.of(); + HttpRequest request = operation.build(properties, input); + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); } - private Map projectEntity(Map raw) { - Map out = new LinkedHashMap<>(); - copyIfPresent(raw, out, "id"); - copyIfPresent(raw, out, "type"); - copyIfPresent(raw, out, "title"); - copyIfPresent(raw, out, "properties"); - return out; - } + private void complete(TaskModel task, String body) throws Exception { + Object parsed = OcgInputs.parseJsonLenient(body); + Object projected = operation.project(parsed); + String serialized = OcgInputs.writeJson(projected); + // Apache StringUtils.abbreviate with a custom marker preserves the + // exact total-length contract callers depend on for context-window + // budgeting; max-width must be ≥ marker length to satisfy its API. + int cap = Math.max(TRUNCATE_MARKER.length(), properties.getResponseCapChars()); + String capped = StringUtils.abbreviate(serialized, TRUNCATE_MARKER, cap); - private Map projectNeighborhood(Map raw) { - Map out = new LinkedHashMap<>(); - copyIfPresent(raw, out, "center"); - copyIfPresent(raw, out, "edges"); - copyIfPresent(raw, out, "neighbors"); - return out; - } - - private Map projectCodeHistory(Map raw) { - Map out = new LinkedHashMap<>(); - copyIfPresent(raw, out, "commits"); - copyIfPresent(raw, out, "repo_id"); - copyIfPresent(raw, out, "path"); - return out; - } - - // ───────────────────────────────────────────────────────────────────── - // Helpers - // ───────────────────────────────────────────────────────────────────── - - private static String trimTrailingSlash(String url) { - if (url == null) return ""; - return url.endsWith("/") ? url.substring(0, url.length() - 1) : url; - } - - private static String urlEncode(String v) { - return URLEncoder.encode(v, StandardCharsets.UTF_8); - } - - private static void copyIfPresent(Map src, Map dst, String key) { - if (src.containsKey(key) && src.get(key) != null) { - dst.put(key, src.get(key)); - } - } - - private static String requiredString(Map input, String key) { - Object v = input.get(key); - if (!(v instanceof String s) || s.isBlank()) { - throw new IllegalArgumentException("Missing or empty required input '" + key + "'"); - } - return s; - } - - private static String stringOrEmpty(Object v) { - return v == null ? "" : String.valueOf(v); - } - - private static int intOr(Object v, int fallback) { - if (v instanceof Number n) return n.intValue(); - if (v instanceof String s) { - try { - return Integer.parseInt(s); - } catch (NumberFormatException ignored) { - return fallback; - } - } - return fallback; - } - - private static Object parseJsonLenient(String body) { - if (body == null || body.isBlank()) return Map.of(); - try { - return MAPPER.readValue(body, Object.class); - } catch (Exception e) { - // Non-JSON bodies (rare) — surface the raw text under a known key. - return Map.of("raw", body); - } - } - - private static String truncateForLog(String body) { - if (body == null) return ""; - return body.length() > 256 ? body.substring(0, 256) + "..." : body; + Map output = new LinkedHashMap<>(); + output.put("result", capped); + output.put("operation", operation.name()); + task.setOutputData(output); + task.setStatus(TaskModel.Status.COMPLETED); } private static void fail(TaskModel task, String reason) { - Map out = new LinkedHashMap<>(); - out.put("error", reason); - task.setOutputData(out); + task.setOutputData(Map.of("error", reason)); task.setReasonForIncompletion(reason); task.setStatus(TaskModel.Status.FAILED); } + + private static HttpClient defaultHttpClient() { + return HttpClient.newBuilder() + .connectTimeout(CONNECT_TIMEOUT) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + } } diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java index 32db7beab..d9eedb43a 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java @@ -10,56 +10,62 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import dev.agentspan.runtime.ocg.operation.OcgCodeHistoryOperation; +import dev.agentspan.runtime.ocg.operation.OcgGetEntityOperation; +import dev.agentspan.runtime.ocg.operation.OcgMemoryDeleteOperation; +import dev.agentspan.runtime.ocg.operation.OcgMemoryReinforceOperation; +import dev.agentspan.runtime.ocg.operation.OcgMemorySetOperation; +import dev.agentspan.runtime.ocg.operation.OcgNeighborhoodOperation; +import dev.agentspan.runtime.ocg.operation.OcgQueryOperation; + /** - * Registers the seven {@code OCG_*} system tasks as Conductor beans when - * {@code agentspan.ocg.url} is set. Bean names match the Conductor task type - * strings so the framework's {@code SystemTaskRegistry} looks them up by type. + * Registers one {@link OcgRequestTask} bean per OCG operation when + * {@code agentspan.ocg.url} is set. Bean names match the Conductor task + * type strings so {@code SystemTaskRegistry} looks them up by type at + * dispatch time. + * + *

Each {@code @Bean} method pairs a fresh {@code OcgRequestTask} with + * a stateless operation strategy — the strategies own the per-endpoint + * URL/method/body/projection details; {@code OcgRequestTask} only knows + * how to send and shape the result.

*/ @Configuration @EnableConfigurationProperties(OcgProperties.class) @ConditionalOnProperty(prefix = "agentspan.ocg", name = "url") public class OcgRequestTaskConfig { - public static final String TASK_TYPE_QUERY = "OCG_QUERY"; - public static final String TASK_TYPE_GET_ENTITY = "OCG_GET_ENTITY"; - public static final String TASK_TYPE_NEIGHBORHOOD = "OCG_NEIGHBORHOOD"; - public static final String TASK_TYPE_CODE_HISTORY = "OCG_CODE_HISTORY"; - public static final String TASK_TYPE_MEMORY_SET = "OCG_MEMORY_SET"; - public static final String TASK_TYPE_MEMORY_REINFORCE = "OCG_MEMORY_REINFORCE"; - public static final String TASK_TYPE_MEMORY_DELETE = "OCG_MEMORY_DELETE"; - - @Bean(TASK_TYPE_QUERY) - public OcgRequestTask ocgQueryTask(OcgProperties props) { - return new OcgRequestTask(TASK_TYPE_QUERY, OcgRequestTask.OP_QUERY, props); + @Bean(OcgQueryOperation.TASK_TYPE) + public OcgRequestTask ocgQueryTask(OcgProperties properties) { + return new OcgRequestTask(new OcgQueryOperation(), properties); } - @Bean(TASK_TYPE_GET_ENTITY) - public OcgRequestTask ocgGetEntityTask(OcgProperties props) { - return new OcgRequestTask(TASK_TYPE_GET_ENTITY, OcgRequestTask.OP_GET_ENTITY, props); + @Bean(OcgGetEntityOperation.TASK_TYPE) + public OcgRequestTask ocgGetEntityTask(OcgProperties properties) { + return new OcgRequestTask(new OcgGetEntityOperation(), properties); } - @Bean(TASK_TYPE_NEIGHBORHOOD) - public OcgRequestTask ocgNeighborhoodTask(OcgProperties props) { - return new OcgRequestTask(TASK_TYPE_NEIGHBORHOOD, OcgRequestTask.OP_NEIGHBORHOOD, props); + @Bean(OcgNeighborhoodOperation.TASK_TYPE) + public OcgRequestTask ocgNeighborhoodTask(OcgProperties properties) { + return new OcgRequestTask(new OcgNeighborhoodOperation(), properties); } - @Bean(TASK_TYPE_CODE_HISTORY) - public OcgRequestTask ocgCodeHistoryTask(OcgProperties props) { - return new OcgRequestTask(TASK_TYPE_CODE_HISTORY, OcgRequestTask.OP_CODE_HISTORY, props); + @Bean(OcgCodeHistoryOperation.TASK_TYPE) + public OcgRequestTask ocgCodeHistoryTask(OcgProperties properties) { + return new OcgRequestTask(new OcgCodeHistoryOperation(), properties); } - @Bean(TASK_TYPE_MEMORY_SET) - public OcgRequestTask ocgMemorySetTask(OcgProperties props) { - return new OcgRequestTask(TASK_TYPE_MEMORY_SET, OcgRequestTask.OP_MEMORY_SET, props); + @Bean(OcgMemorySetOperation.TASK_TYPE) + public OcgRequestTask ocgMemorySetTask(OcgProperties properties) { + return new OcgRequestTask(new OcgMemorySetOperation(), properties); } - @Bean(TASK_TYPE_MEMORY_REINFORCE) - public OcgRequestTask ocgMemoryReinforceTask(OcgProperties props) { - return new OcgRequestTask(TASK_TYPE_MEMORY_REINFORCE, OcgRequestTask.OP_MEMORY_REINFORCE, props); + @Bean(OcgMemoryReinforceOperation.TASK_TYPE) + public OcgRequestTask ocgMemoryReinforceTask(OcgProperties properties) { + return new OcgRequestTask(new OcgMemoryReinforceOperation(), properties); } - @Bean(TASK_TYPE_MEMORY_DELETE) - public OcgRequestTask ocgMemoryDeleteTask(OcgProperties props) { - return new OcgRequestTask(TASK_TYPE_MEMORY_DELETE, OcgRequestTask.OP_MEMORY_DELETE, props); + @Bean(OcgMemoryDeleteOperation.TASK_TYPE) + public OcgRequestTask ocgMemoryDeleteTask(OcgProperties properties) { + return new OcgRequestTask(new OcgMemoryDeleteOperation(), properties); } } diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java new file mode 100644 index 000000000..bfdf12447 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg.operation; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.util.Map; + +import dev.agentspan.runtime.ocg.OcgProperties; + +/** {@code GET /api/v1/code/history/{repo_id}?path=...&limit=N} — file commit history. */ +public final class OcgCodeHistoryOperation implements OcgOperation { + + public static final String TASK_TYPE = "OCG_CODE_HISTORY"; + public static final String NAME = "code_history"; + + private static final int DEFAULT_LIMIT = 20; + + @Override + public String taskType() { + return TASK_TYPE; + } + + @Override + public String name() { + return NAME; + } + + @Override + public HttpRequest build(OcgProperties properties, Map input) { + String repoId = OcgInputs.required(input, "repo_id"); + String path = OcgInputs.required(input, "path"); + URI uri = OcgUri.forApi(properties) + .pathSegment("code", "history", repoId) + .queryParam("path", path) + .queryParam("limit", OcgInputs.intOrDefault(input.get("limit"), DEFAULT_LIMIT)) + .build() + .toUri(); + return OcgRequest.get(properties, uri); + } + + @Override + public Object project(Object raw) { + if (!(raw instanceof Map map)) return raw; + return OcgInputs.pick(map, "commits", "repo_id", "path"); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java new file mode 100644 index 000000000..fad99aed0 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg.operation; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.util.Map; + +import dev.agentspan.runtime.ocg.OcgProperties; + +/** {@code GET /api/v1/entities/{entity_id}} — single entity lookup by id. */ +public final class OcgGetEntityOperation implements OcgOperation { + + public static final String TASK_TYPE = "OCG_GET_ENTITY"; + public static final String NAME = "get_entity"; + + @Override + public String taskType() { + return TASK_TYPE; + } + + @Override + public String name() { + return NAME; + } + + @Override + public HttpRequest build(OcgProperties properties, Map input) { + String entityId = OcgInputs.required(input, "entity_id"); + URI uri = OcgUri.forApi(properties) + .pathSegment("entities", entityId) + .build() + .toUri(); + return OcgRequest.get(properties, uri); + } + + @Override + public Object project(Object raw) { + if (!(raw instanceof Map map)) return raw; + return OcgInputs.pick(map, "id", "type", "title", "properties"); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java new file mode 100644 index 000000000..0a1f48584 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg.operation; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.math.NumberUtils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Map and JSON utilities shared by every {@link OcgOperation} and by + * {@code OcgRequestTask}. Stateless; Apache Commons wherever it gives a + * cleaner one-liner than rolling our own. + * + *

Owns the single {@link ObjectMapper} used across the OCG subsystem so + * there's one source of truth for JSON shape and no per-class instances.

+ */ +public final class OcgInputs { + + /** Shared Jackson mapper. {@link ObjectMapper} is thread-safe. */ + public static final ObjectMapper MAPPER = new ObjectMapper(); + + private OcgInputs() {} + + /** + * Build a new {@link LinkedHashMap} containing only the requested keys + * that are present and non-null in {@code src}. Insertion order is the + * key order passed in — matters for stable JSON serialization of + * request bodies. + */ + public static Map pick(Map src, String... keys) { + Map out = new LinkedHashMap<>(); + for (String key : keys) { + Object value = src.get(key); + if (value != null) { + out.put(key, value); + } + } + return out; + } + + /** + * Extract a required string input. Throws {@link IllegalArgumentException} + * (via {@link Validate}) with a consistent message when missing or blank, + * giving the OCG sub-agent's LLM a debuggable failure reason instead of + * an opaque NPE downstream. + */ + public static String required(Map input, String key) { + Object value = input.get(key); + Validate.isTrue( + value instanceof String && StringUtils.isNotBlank((String) value), + "Missing or empty required input '%s'", + key); + return (String) value; + } + + /** + * Coerce {@code value} to int, falling back when it's null, non-numeric, + * or an unparseable string. Number → intValue(), String → parsed via + * {@link NumberUtils#toInt(String, int)} so we don't throw on bad input. + */ + public static int intOrDefault(Object value, int fallback) { + if (value instanceof Number n) return n.intValue(); + if (value instanceof String s) return NumberUtils.toInt(s, fallback); + return fallback; + } + + /** + * Parse a JSON body without throwing on malformed input. Blank → empty + * map; parse failure → {@code {"raw": }} so the original text is + * still visible downstream rather than swallowed. + */ + public static Object parseJsonLenient(String body) { + if (StringUtils.isBlank(body)) return Map.of(); + try { + return MAPPER.readValue(body, Object.class); + } catch (Exception e) { + return Map.of("raw", body); + } + } + + /** Serialize an object to JSON using the shared mapper. */ + public static String writeJson(Object value) throws JsonProcessingException { + return MAPPER.writeValueAsString(value); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java new file mode 100644 index 000000000..349f9f543 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg.operation; + +import java.net.http.HttpRequest; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.util.UriComponentsBuilder; + +import dev.agentspan.runtime.ocg.OcgProperties; + +/** {@code DELETE /api/v1/memories/{key}?agent=...&user=...} — remove a memory by key. */ +public final class OcgMemoryDeleteOperation implements OcgOperation { + + public static final String TASK_TYPE = "OCG_MEMORY_DELETE"; + public static final String NAME = "memory_delete"; + + @Override + public String taskType() { + return TASK_TYPE; + } + + @Override + public String name() { + return NAME; + } + + @Override + public HttpRequest build(OcgProperties properties, Map input) { + String key = OcgInputs.required(input, "key"); + UriComponentsBuilder uri = OcgUri.forApi(properties).pathSegment("memories", key); + addQueryParamIfPresent(uri, "agent", input.get("agent")); + addQueryParamIfPresent(uri, "user", input.get("user")); + return OcgRequest.delete(properties, uri.build().toUri()); + } + + private static void addQueryParamIfPresent(UriComponentsBuilder uri, String name, Object value) { + String s = StringUtils.defaultString(value == null ? null : value.toString()); + if (StringUtils.isNotEmpty(s)) { + uri.queryParam(name, s); + } + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java new file mode 100644 index 000000000..82003e57d --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg.operation; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.util.Map; + +import dev.agentspan.runtime.ocg.OcgProperties; + +/** {@code POST /api/v1/memories/{key}/reinforce} — confidence boost on re-observation. */ +public final class OcgMemoryReinforceOperation implements OcgOperation { + + public static final String TASK_TYPE = "OCG_MEMORY_REINFORCE"; + public static final String NAME = "memory_reinforce"; + + @Override + public String taskType() { + return TASK_TYPE; + } + + @Override + public String name() { + return NAME; + } + + @Override + public HttpRequest build(OcgProperties properties, Map input) throws Exception { + String key = OcgInputs.required(input, "key"); + Map body = OcgInputs.pick(input, "agent", "user", "confidence_boost", "source_ref"); + URI uri = OcgUri.forApi(properties) + .pathSegment("memories", key, "reinforce") + .build() + .toUri(); + return OcgRequest.postJson(properties, uri, body); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java new file mode 100644 index 000000000..1fe1de75a --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg.operation; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.util.LinkedHashMap; +import java.util.Map; + +import dev.agentspan.runtime.ocg.OcgProperties; + +/** + * {@code POST /api/v1/memories} — create or overwrite a memory. + * + *

Body is the input map verbatim minus the {@code __agentspan_ctx__} + * execution-token glob, which is server-side plumbing and should never be + * forwarded to OCG. Identity projection — OCG's memory response is small + * enough to surface unchanged.

+ */ +public final class OcgMemorySetOperation implements OcgOperation { + + public static final String TASK_TYPE = "OCG_MEMORY_SET"; + public static final String NAME = "memory_set"; + + @Override + public String taskType() { + return TASK_TYPE; + } + + @Override + public String name() { + return NAME; + } + + @Override + public HttpRequest build(OcgProperties properties, Map input) throws Exception { + Map body = new LinkedHashMap<>(input); + body.remove("__agentspan_ctx__"); + URI uri = OcgUri.forApi(properties).pathSegment("memories").build().toUri(); + return OcgRequest.postJson(properties, uri, body); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java new file mode 100644 index 000000000..4f43b9bce --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg.operation; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.util.Map; + +import dev.agentspan.runtime.ocg.OcgProperties; + +/** {@code GET /api/v1/graph/neighborhood/{entity_id}?depth=N&limit=M} — graph traversal. */ +public final class OcgNeighborhoodOperation implements OcgOperation { + + public static final String TASK_TYPE = "OCG_NEIGHBORHOOD"; + public static final String NAME = "neighborhood"; + + private static final int DEFAULT_DEPTH = 2; + private static final int DEFAULT_LIMIT = 50; + + @Override + public String taskType() { + return TASK_TYPE; + } + + @Override + public String name() { + return NAME; + } + + @Override + public HttpRequest build(OcgProperties properties, Map input) { + String entityId = OcgInputs.required(input, "entity_id"); + URI uri = OcgUri.forApi(properties) + .pathSegment("graph", "neighborhood", entityId) + .queryParam("depth", OcgInputs.intOrDefault(input.get("depth"), DEFAULT_DEPTH)) + .queryParam("limit", OcgInputs.intOrDefault(input.get("limit"), DEFAULT_LIMIT)) + .build() + .toUri(); + return OcgRequest.get(properties, uri); + } + + @Override + public Object project(Object raw) { + if (!(raw instanceof Map map)) return raw; + return OcgInputs.pick(map, "center", "edges", "neighbors"); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java new file mode 100644 index 000000000..a63822675 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg.operation; + +import java.net.http.HttpRequest; +import java.util.Map; + +import dev.agentspan.runtime.ocg.OcgProperties; + +/** + * Strategy for a single OCG endpoint. One implementation per {@code OCG_*} + * task type; each owns the URL/method/body for its endpoint plus the + * field-projection rule for shrinking the raw response before it reaches + * the LLM. + * + *

Implementations are stateless and reused across calls — the per-call + * inputs flow in via {@link #build}, never via constructors.

+ */ +public interface OcgOperation { + + /** Conductor task type string this operation registers under (e.g. {@code "OCG_QUERY"}). */ + String taskType(); + + /** Short operation name used in logs and the task output's {@code operation} field. */ + String name(); + + /** + * Build the HTTP request for this operation. Implementations should + * use {@link OcgRequest} and {@link OcgUri} so authentication headers + * and base-URL handling stay consistent across endpoints. + */ + HttpRequest build(OcgProperties properties, Map input) throws Exception; + + /** + * Project the parsed JSON response down to the fields the LLM needs. + * Default is identity — only implementations that strip noise (e.g. + * scoring metadata, internal ids) need to override. + */ + default Object project(Object rawResponse) { + return rawResponse; + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java new file mode 100644 index 000000000..f18a1aed4 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg.operation; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import dev.agentspan.runtime.ocg.OcgProperties; + +/** {@code POST /api/v1/agent/query} — natural-language retrieval over the graph. */ +public final class OcgQueryOperation implements OcgOperation { + + public static final String TASK_TYPE = "OCG_QUERY"; + public static final String NAME = "query"; + + @Override + public String taskType() { + return TASK_TYPE; + } + + @Override + public String name() { + return NAME; + } + + @Override + public HttpRequest build(OcgProperties properties, Map input) throws Exception { + Map body = + OcgInputs.pick(input, "query", "max_results", "traversal_level", "start_time", "end_time"); + URI uri = + OcgUri.forApi(properties).pathSegment("agent", "query").build().toUri(); + return OcgRequest.postJson(properties, uri, body); + } + + /** + * Keep citations (the only piece the LLM acts on) and traversal_results + * when present; drop scoring metadata, embedding vectors, and any other + * fields the OCG service may add over time. + */ + @Override + public Object project(Object raw) { + if (!(raw instanceof Map map)) return raw; + Map out = new LinkedHashMap<>(); + out.put("citations", projectCitations(map.get("citations"))); + if (map.containsKey("traversal_results")) { + out.put("traversal_results", map.get("traversal_results")); + } + return out; + } + + private static List> projectCitations(Object raw) { + if (!(raw instanceof List list)) return List.of(); + List> out = new ArrayList<>(); + for (Object item : list) { + if (item instanceof Map citation) { + out.add(OcgInputs.pick(citation, "source_item_id", "title", "container_id", "snippet")); + } + } + return out; + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java new file mode 100644 index 000000000..aa1e60a8b --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg.operation; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.nio.charset.StandardCharsets; +import java.time.Duration; + +import dev.agentspan.runtime.ocg.OcgProperties; + +/** + * HTTP request factory for OCG endpoints. Centralises the bearer-auth + * header, default timeouts, and JSON body serialization so every + * {@link OcgOperation} produces requests with the same baseline headers + * and policies — drift here would mean some endpoints get authenticated + * and others silently don't. + */ +public final class OcgRequest { + + private static final Duration READ_TIMEOUT = Duration.ofSeconds(30); + + private OcgRequest() {} + + /** + * Common {@link HttpRequest.Builder} for every OCG endpoint: read + * timeout, JSON accept, and the optional bearer token. Operations call + * {@code .uri(...).METHOD(...).build()} on the returned builder. + */ + public static HttpRequest.Builder base(OcgProperties properties) { + HttpRequest.Builder b = HttpRequest.newBuilder().timeout(READ_TIMEOUT).header("Accept", "application/json"); + if (properties.hasApiKey()) { + b.header("Authorization", "Bearer " + properties.getApiKey()); + } + return b; + } + + /** POST with a JSON-serialized body. */ + public static HttpRequest postJson(OcgProperties properties, URI uri, Object body) throws IOException { + String json = OcgInputs.writeJson(body); + return base(properties) + .uri(uri) + .header("Content-Type", "application/json") + .POST(BodyPublishers.ofString(json, StandardCharsets.UTF_8)) + .build(); + } + + /** GET with no body. */ + public static HttpRequest get(OcgProperties properties, URI uri) { + return base(properties).uri(uri).GET().build(); + } + + /** DELETE with no body. */ + public static HttpRequest delete(OcgProperties properties, URI uri) { + return base(properties).uri(uri).DELETE().build(); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java new file mode 100644 index 000000000..247ff634f --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg.operation; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.util.UriComponentsBuilder; + +import dev.agentspan.runtime.ocg.OcgProperties; + +/** + * URI builder for OCG endpoints. Every OCG path sits under {@code /api/v1} + * on the configured base URL; this helper handles the trailing-slash + * trimming and the prefix attachment so operations only spell out the + * endpoint-specific path segments. + * + *

Path segments and query params go through {@link UriComponentsBuilder}, + * which URL-encodes correctly — no hand-rolled {@code URLEncoder} calls + * scattered across operations.

+ */ +public final class OcgUri { + + /** OCG's stable API version prefix. Bump as a single point of change. */ + public static final String API_PREFIX_V1 = "/api/v1"; + + private OcgUri() {} + + /** + * Returns a {@link UriComponentsBuilder} rooted at + * {@code /api/v1}. Operations chain {@code .pathSegment(...)} + * + {@code .queryParam(...)} on top. + */ + public static UriComponentsBuilder forApi(OcgProperties properties) { + String base = StringUtils.removeEnd(StringUtils.defaultString(properties.getUrl()), "/"); + return UriComponentsBuilder.fromUriString(base + API_PREFIX_V1); + } +} diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java index 10e73d200..aba36e5d0 100644 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java @@ -9,6 +9,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import java.net.http.HttpClient; @@ -21,12 +23,18 @@ import com.netflix.conductor.model.TaskModel; +import dev.agentspan.runtime.ocg.operation.OcgGetEntityOperation; +import dev.agentspan.runtime.ocg.operation.OcgMemoryDeleteOperation; +import dev.agentspan.runtime.ocg.operation.OcgNeighborhoodOperation; +import dev.agentspan.runtime.ocg.operation.OcgQueryOperation; + /** * Unit tests for {@link OcgRequestTask}. * - *

Each operation is exercised against a mocked {@link HttpClient} to pin the - * URL/method contract with the OCG service and the projection + capping - * behaviour.

+ *

Exercises the thin orchestrator with each strategy plugged in to + * pin the per-endpoint URL/method contract, projection, capping, and + * error handling. End-to-end behaviour through the strategy is the + * surface most likely to drift when an operation is refactored.

*/ class OcgRequestTaskTest { @@ -59,13 +67,13 @@ private static TaskModel taskWith(Map input) { void queryOperationPostsToAgentQueryEndpoint() throws Exception { HttpClient http = mock(HttpClient.class); stubSend(http, stub(200, "{\"citations\":[{\"source_item_id\":\"a\",\"title\":\"t1\",\"snippet\":\"s\"}]}")); - OcgRequestTask task = new OcgRequestTask("OCG_QUERY", OcgRequestTask.OP_QUERY, props("http://ocg.local"), http); + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props("http://ocg.local"), http); TaskModel t = taskWith(Map.of("query", "find foo", "max_results", 50)); task.start(null, t, null); ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - org.mockito.Mockito.verify(http).send(req.capture(), any()); + verify(http).send(req.capture(), any()); assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/api/v1/agent/query"); assertThat(req.getValue().method()).isEqualTo("POST"); assertThat(t.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); @@ -80,14 +88,13 @@ void queryOperationPostsToAgentQueryEndpoint() throws Exception { void getEntityOperationGetsToEntitiesEndpoint() throws Exception { HttpClient http = mock(HttpClient.class); stubSend(http, stub(200, "{\"id\":\"e1\",\"type\":\"message\",\"title\":\"hello\"}")); - OcgRequestTask task = - new OcgRequestTask("OCG_GET_ENTITY", OcgRequestTask.OP_GET_ENTITY, props("http://ocg.local/"), http); + OcgRequestTask task = new OcgRequestTask(new OcgGetEntityOperation(), props("http://ocg.local/"), http); TaskModel t = taskWith(Map.of("entity_id", "e1")); task.start(null, t, null); ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - org.mockito.Mockito.verify(http).send(req.capture(), any()); + verify(http).send(req.capture(), any()); // Trailing slash on the configured URL is trimmed. assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/api/v1/entities/e1"); assertThat(req.getValue().method()).isEqualTo("GET"); @@ -98,14 +105,13 @@ void getEntityOperationGetsToEntitiesEndpoint() throws Exception { void neighborhoodOperationIncludesDepthAndLimitQueryParams() throws Exception { HttpClient http = mock(HttpClient.class); stubSend(http, stub(200, "{\"center\":{\"id\":\"e1\"},\"edges\":[]}")); - OcgRequestTask task = - new OcgRequestTask("OCG_NEIGHBORHOOD", OcgRequestTask.OP_NEIGHBORHOOD, props("http://ocg.local"), http); + OcgRequestTask task = new OcgRequestTask(new OcgNeighborhoodOperation(), props("http://ocg.local"), http); TaskModel t = taskWith(Map.of("entity_id", "e1", "depth", 1, "limit", 5)); task.start(null, t, null); ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - org.mockito.Mockito.verify(http).send(req.capture(), any()); + verify(http).send(req.capture(), any()); assertThat(req.getValue().uri().toString()) .isEqualTo("http://ocg.local/api/v1/graph/neighborhood/e1?depth=1&limit=5"); } @@ -114,25 +120,28 @@ void neighborhoodOperationIncludesDepthAndLimitQueryParams() throws Exception { void memoryDeleteOperationDispatchesDeleteWithQueryString() throws Exception { HttpClient http = mock(HttpClient.class); stubSend(http, stub(200, "{\"deleted\":true}")); - OcgRequestTask task = new OcgRequestTask( - "OCG_MEMORY_DELETE", OcgRequestTask.OP_MEMORY_DELETE, props("http://ocg.local"), http); + OcgRequestTask task = new OcgRequestTask(new OcgMemoryDeleteOperation(), props("http://ocg.local"), http); TaskModel t = taskWith(Map.of("key", "k1", "agent", "agent:foo", "user", "user:bar")); task.start(null, t, null); ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - org.mockito.Mockito.verify(http).send(req.capture(), any()); + verify(http).send(req.capture(), any()); assertThat(req.getValue().method()).isEqualTo("DELETE"); - // URL-encoded query params; agent and user appear in declared order. + // Spring's UriComponentsBuilder encodes only characters that RFC 3986 + // reserves for query values — ``:`` is not reserved there, so it + // stays raw. The previous hand-rolled URLEncoder.encode was over- + // encoding to ``%3A``; OCG accepts both, but the standards-compliant + // form is what the new builder produces. assertThat(req.getValue().uri().toString()) - .isEqualTo("http://ocg.local/api/v1/memories/k1?agent=agent%3Afoo&user=user%3Abar"); + .isEqualTo("http://ocg.local/api/v1/memories/k1?agent=agent:foo&user=user:bar"); } @Test void non2xxResponseSurfacesAsFailedStatus() throws Exception { HttpClient http = mock(HttpClient.class); stubSend(http, stub(503, "service unavailable")); - OcgRequestTask task = new OcgRequestTask("OCG_QUERY", OcgRequestTask.OP_QUERY, props("http://ocg.local"), http); + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props("http://ocg.local"), http); TaskModel t = taskWith(Map.of("query", "x")); task.start(null, t, null); @@ -161,7 +170,7 @@ void responseLargerThanCapIsTruncatedWithSuffix() throws Exception { stubSend(http, stub(200, big.toString())); OcgProperties p = props("http://ocg.local"); p.setResponseCapChars(256); - OcgRequestTask task = new OcgRequestTask("OCG_QUERY", OcgRequestTask.OP_QUERY, p, http); + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), p, http); TaskModel t = taskWith(Map.of("query", "x")); task.start(null, t, null); @@ -178,13 +187,13 @@ void authorizationHeaderAttachedWhenApiKeySet() throws Exception { stubSend(http, stub(200, "{\"citations\":[]}")); OcgProperties p = props("http://ocg.local"); p.setApiKey("secret-key-123"); - OcgRequestTask task = new OcgRequestTask("OCG_QUERY", OcgRequestTask.OP_QUERY, p, http); + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), p, http); TaskModel t = taskWith(Map.of("query", "x")); task.start(null, t, null); ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - org.mockito.Mockito.verify(http).send(req.capture(), any()); + verify(http).send(req.capture(), any()); // Bearer scheme — pinning both the header name and the prefix so a // refactor can't silently change to e.g. ``X-API-Key`` without // tripping a test. @@ -197,20 +206,20 @@ void noAuthorizationHeaderWhenApiKeyUnset() throws Exception { stubSend(http, stub(200, "{\"citations\":[]}")); // props(...) does not set an api key → header must be omitted so // unauthenticated local OCG instances keep working. - OcgRequestTask task = new OcgRequestTask("OCG_QUERY", OcgRequestTask.OP_QUERY, props("http://ocg.local"), http); + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props("http://ocg.local"), http); TaskModel t = taskWith(Map.of("query", "x")); task.start(null, t, null); ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - org.mockito.Mockito.verify(http).send(req.capture(), any()); + verify(http).send(req.capture(), any()); assertThat(req.getValue().headers().firstValue("Authorization")).isEmpty(); } @Test void disabledPropertiesYieldsFailedTask() throws Exception { HttpClient http = mock(HttpClient.class); - OcgRequestTask task = new OcgRequestTask("OCG_QUERY", OcgRequestTask.OP_QUERY, props(""), http); + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(""), http); TaskModel t = taskWith(Map.of("query", "x")); task.start(null, t, null); @@ -218,6 +227,6 @@ void disabledPropertiesYieldsFailedTask() throws Exception { assertThat(t.getStatus()).isEqualTo(TaskModel.Status.FAILED); assertThat(t.getReasonForIncompletion()).contains("not configured"); // Importantly: no HTTP call attempted when disabled. - org.mockito.Mockito.verifyNoInteractions(http); + verifyNoInteractions(http); } } From 5cc0a7e6bfe226e1b530d439c95cb59a7e73dc4d Mon Sep 17 00:00:00 2001 From: nicholascole Date: Tue, 9 Jun 2026 12:43:19 -0700 Subject: [PATCH 07/61] refactor(server): generic registry for server-registered agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace OCG's bespoke @PostConstruct registration with a generic two-bean registry that any future server-side sub-agent can plug into without writing per-feature service code. Generic infrastructure (runtime/registry/, OCG-agnostic): - RegisteredAgent — interface; agentConfig() + autoExpose() - RegisteredAgentRegistrar — @PostConstruct picks up every bean, compiles via AgentCompiler, stamps the auto-expose marker when requested, and writes to MetadataDAO - RegisteredTaskDefs — interface; taskDefs() - RegisteredTaskDefsRegistrar — runs first via @DependsOn OCG plug-in (just two @Components — no @Configuration wrapper): - OcgRegisteredAgent — implements RegisteredAgent - OcgRegisteredTaskDefs — implements RegisteredTaskDefs - OcgSubAgentService — DELETED (responsibilities moved to the two registrars; no per-feature @PostConstruct anywhere now) Both @Components carry @ConditionalOnExpression rather than @ConditionalOnProperty: the latter treats empty strings as "present and not false", so empty OCG_URL would still instantiate the beans and leak the auto-expose registration into the test DB. The same conditional is now on OcgRequestTaskConfig for consistency. Two new test classes (6 tests total) pin the contract every future sub-agent relies on: compile, stamp-if-exposed, persist; no stamp when autoExpose() returns null; empty supplier lists are no-ops. End-to-end OCG smoke test on the live dev instance still works identically — same shape, same citations, same token count. --- .../runtime/ocg/OcgRegisteredAgent.java | 49 ++++++ .../runtime/ocg/OcgRegisteredTaskDefs.java | 64 ++++++++ .../runtime/ocg/OcgRequestTaskConfig.java | 6 +- .../runtime/ocg/OcgSubAgentService.java | 124 --------------- .../runtime/registry/RegisteredAgent.java | 50 ++++++ .../registry/RegisteredAgentRegistrar.java | 89 +++++++++++ .../runtime/registry/RegisteredTaskDefs.java | 27 ++++ .../registry/RegisteredTaskDefsRegistrar.java | 57 +++++++ .../RegisteredAgentRegistrarTest.java | 145 ++++++++++++++++++ .../RegisteredTaskDefsRegistrarTest.java | 61 ++++++++ 10 files changed, 546 insertions(+), 126 deletions(-) create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java create mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java delete mode 100644 server/src/main/java/dev/agentspan/runtime/ocg/OcgSubAgentService.java create mode 100644 server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java create mode 100644 server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java create mode 100644 server/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java create mode 100644 server/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java create mode 100644 server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java create mode 100644 server/src/test/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrarTest.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java new file mode 100644 index 000000000..c879d0b2e --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; + +import dev.agentspan.runtime.model.AgentConfig; +import dev.agentspan.runtime.registry.RegisteredAgent; + +import lombok.RequiredArgsConstructor; + +/** + * OCG's contribution to the {@link RegisteredAgent} registry: the + * server-registered sub-agent that the main agent's LLM can delegate to + * for context retrieval. + * + *

Picked up automatically by {@code RegisteredAgentRegistrar} on boot, + * compiled to a {@code WorkflowDef}, stamped with the auto-expose + * metadata marker (via {@link #autoExpose()}), and written to the + * metadata store. From the next user-agent compile onward, + * {@code ocg_agent} appears in every LLM's tool list.

+ * + *

The {@code @ConditionalOnExpression} uses {@code .length() > 0} + * rather than the more obvious {@code @ConditionalOnProperty} because + * the latter treats an empty string as "present and not false" and + * would instantiate this bean for unset {@code OCG_URL}, breaking the + * tests that rely on OCG being off by default.

+ */ +@Component +@ConditionalOnExpression("'${agentspan.ocg.url:}'.length() > 0") +@RequiredArgsConstructor +public class OcgRegisteredAgent implements RegisteredAgent { + + private final OcgProperties properties; + + @Override + public AgentConfig agentConfig() { + return OcgAgentFactory.build(properties); + } + + @Override + public ExposeAsTool autoExpose() { + return new ExposeAsTool(OcgAgentFactory.TOOL_NAME, OcgAgentFactory.TOOL_DESCRIPTION); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java new file mode 100644 index 000000000..e552eb40c --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import java.util.List; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; + +import com.netflix.conductor.common.metadata.tasks.TaskDef; + +import dev.agentspan.runtime.ocg.operation.OcgCodeHistoryOperation; +import dev.agentspan.runtime.ocg.operation.OcgGetEntityOperation; +import dev.agentspan.runtime.ocg.operation.OcgMemoryDeleteOperation; +import dev.agentspan.runtime.ocg.operation.OcgMemoryReinforceOperation; +import dev.agentspan.runtime.ocg.operation.OcgMemorySetOperation; +import dev.agentspan.runtime.ocg.operation.OcgNeighborhoodOperation; +import dev.agentspan.runtime.ocg.operation.OcgQueryOperation; +import dev.agentspan.runtime.registry.RegisteredTaskDefs; + +/** + * OCG's contribution to the {@link RegisteredTaskDefs} registry. + * + *

Conductor resolves dynamic-fork tasks by name; the seven + * {@code ocg_*} TaskDefs registered here let the OCG sub-agent's tool + * calls dispatch successfully at runtime. {@code retryCount = 0} on + * purpose — each call is a stateless HTTP round-trip handled by + * {@code OcgRequestTask} and the parent LLM loop owns retry decisions.

+ */ +@Component +@ConditionalOnExpression("'${agentspan.ocg.url:}'.length() > 0") +public class OcgRegisteredTaskDefs implements RegisteredTaskDefs { + + private static final int OCG_TASK_TIMEOUT_SECONDS = 60; + private static final int OCG_TASK_RETRY_COUNT = 0; + private static final String OCG_TASK_OWNER_EMAIL = "ocg@agentspan.dev"; + + private static final List TASK_NAMES = List.of( + OcgQueryOperation.NAME, + OcgGetEntityOperation.NAME, + OcgNeighborhoodOperation.NAME, + OcgCodeHistoryOperation.NAME, + OcgMemorySetOperation.NAME, + OcgMemoryReinforceOperation.NAME, + OcgMemoryDeleteOperation.NAME); + + @Override + public List taskDefs() { + return TASK_NAMES.stream().map(OcgRegisteredTaskDefs::buildTaskDef).toList(); + } + + private static TaskDef buildTaskDef(String name) { + TaskDef def = new TaskDef(); + def.setName(name); + def.setRetryCount(OCG_TASK_RETRY_COUNT); + def.setTimeoutSeconds(OCG_TASK_TIMEOUT_SECONDS); + def.setResponseTimeoutSeconds(OCG_TASK_TIMEOUT_SECONDS); + def.setOwnerEmail(OCG_TASK_OWNER_EMAIL); + return def; + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java index d9eedb43a..59e6322e6 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java @@ -5,7 +5,7 @@ package dev.agentspan.runtime.ocg; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -31,7 +31,9 @@ */ @Configuration @EnableConfigurationProperties(OcgProperties.class) -@ConditionalOnProperty(prefix = "agentspan.ocg", name = "url") +// Empty agentspan.ocg.url means "feature off" — must use an expression +// because @ConditionalOnProperty matches empty strings. +@ConditionalOnExpression("'${agentspan.ocg.url:}'.length() > 0") public class OcgRequestTaskConfig { @Bean(OcgQueryOperation.TASK_TYPE) diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgSubAgentService.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgSubAgentService.java deleted file mode 100644 index 517773dce..000000000 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgSubAgentService.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import jakarta.annotation.PostConstruct; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Service; - -import com.netflix.conductor.common.metadata.tasks.TaskDef; -import com.netflix.conductor.common.metadata.workflow.WorkflowDef; -import com.netflix.conductor.dao.MetadataDAO; - -import dev.agentspan.runtime.compiler.AgentCompiler; -import dev.agentspan.runtime.model.AgentConfig; - -import lombok.RequiredArgsConstructor; - -/** - * Registers the OCG sub-agent workflow at startup (when - * {@code agentspan.ocg.url} is set). - * - *

The agent itself is built by {@link OcgAgentFactory}, compiled by the - * standard {@link AgentCompiler}, and persisted via {@link MetadataDAO}. From - * Conductor's perspective it is just another workflow named - * {@code _ocg_agent}.

- * - *

Main-agent invocation happens via an {@code agent_tool} that - * {@code AgentService} auto-injects into every top-level agent — the main - * agent's LLM decides whether and when to call it. This service no longer - * dispatches OCG itself; it only owns the workflow's registration.

- */ -@Service -@ConditionalOnProperty(prefix = "agentspan.ocg", name = "url") -@RequiredArgsConstructor -public class OcgSubAgentService { - - private static final Logger log = LoggerFactory.getLogger(OcgSubAgentService.class); - - private final OcgProperties properties; - private final AgentCompiler agentCompiler; - private final MetadataDAO metadataDAO; - - @PostConstruct - public void registerWorkflow() { - if (!properties.isEnabled()) { - // Defensive — the @ConditionalOnProperty guard means we shouldn't - // be here, but keep the check so unit tests can instantiate this - // service directly with a disabled config without crashing. - return; - } - registerOcgTaskDefs(); - AgentConfig config = OcgAgentFactory.build(properties); - WorkflowDef def = agentCompiler.compile(config); - - // Stamp the auto-expose marker so AgentCompiler.mergeAutoExposedTools - // appends this workflow as an `ocg_agent` agent_tool on every - // subsequent top-level compile. This is the only line that ties OCG - // to LLM visibility — the rest is generic compiler machinery. - Map metadata = - def.getMetadata() != null ? new LinkedHashMap<>(def.getMetadata()) : new LinkedHashMap<>(); - metadata.put( - AgentCompiler.AUTO_EXPOSE_AS_TOOL_METADATA_KEY, - Map.of("name", OcgAgentFactory.TOOL_NAME, "description", OcgAgentFactory.TOOL_DESCRIPTION)); - def.setMetadata(metadata); - - metadataDAO.updateWorkflowDef(def); - log.info( - "OCG sub-agent registered: workflow='{}' tool='{}' model='{}' url='{}'", - def.getName(), - OcgAgentFactory.TOOL_NAME, - properties.getModel(), - properties.getUrl()); - } - - /** - * Register a {@link TaskDef} for each OCG tool name so Conductor's dynamic - * dispatch can resolve the task at runtime. - * - *

The enrichment script (see {@code JavaScriptBuilder.enrichToolsScript}) - * emits dynamic tasks with {@code name=} and - * {@code type=OCG_*}. Conductor looks up the task by name in the - * TaskDef registry before dispatching — without a matching def, the - * SUB_WORKFLOW fails with {@code "Cannot find task by name ocg_query in - * the task definitions"} and the parent LLM loop sees an opaque error.

- * - *

{@code retryCount=0} on purpose: each OCG call is a stateless HTTP - * round-trip handled by {@link OcgRequestTask}; retries here would double - * the load and bypass the parent LLM's ability to refine the query.

- */ - private void registerOcgTaskDefs() { - List names = List.of( - "ocg_query", - "ocg_get_entity", - "ocg_neighborhood", - "ocg_code_history", - "ocg_memory_set", - "ocg_memory_reinforce", - "ocg_memory_delete"); - for (String name : names) { - TaskDef def = new TaskDef(); - def.setName(name); - def.setRetryCount(0); - def.setTimeoutSeconds(60); - def.setResponseTimeoutSeconds(60); - def.setOwnerEmail("ocg@agentspan.dev"); - metadataDAO.updateTaskDef(def); - } - log.info("OCG TaskDefs registered: {}", names); - } - - public boolean isEnabled() { - return properties.isEnabled(); - } -} diff --git a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java new file mode 100644 index 000000000..1f130af8b --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.registry; + +import dev.agentspan.runtime.model.AgentConfig; + +/** + * Marker for a Spring bean that contributes a server-registered agent. + * + *

Any {@code @Bean} declared as {@link RegisteredAgent} is picked up by + * {@link RegisteredAgentRegistrar} on startup, compiled into a + * {@code WorkflowDef}, and persisted to Conductor's metadata store. Adding + * a new server-side sub-agent is therefore a one-bean change — no + * per-feature {@code @PostConstruct}, no manual {@code MetadataDAO} + * write, no duplication of the compile/stamp ceremony.

+ * + *

Implementations should be stateless or read configuration via Spring + * injection. {@link #agentConfig()} is invoked exactly once at startup.

+ */ +public interface RegisteredAgent { + + /** + * The agent definition to compile and register. The returned + * {@link AgentConfig} owns name, model, instructions, tools — the + * registrar does not touch any of these fields. + */ + AgentConfig agentConfig(); + + /** + * When non-null, the registrar stamps an auto-expose marker on the + * compiled {@code WorkflowDef} so {@code AgentCompiler.mergeAutoExposedTools} + * appends this agent as an {@code agent_tool} on every top-level + * user-agent compile. Return {@code null} to register the workflow + * without exposing it as a tool. + */ + default ExposeAsTool autoExpose() { + return null; + } + + /** + * The LLM-facing name and description used by the auto-expose path. + * The name is what users' agents will see in their tool spec list; + * the description is the LLM's only hint about when to + * delegate. + */ + record ExposeAsTool(String toolName, String toolDescription) {} +} diff --git a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java new file mode 100644 index 000000000..9f59d8cbe --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.registry; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import jakarta.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.DependsOn; +import org.springframework.stereotype.Component; + +import com.netflix.conductor.common.metadata.workflow.WorkflowDef; +import com.netflix.conductor.dao.MetadataDAO; + +import dev.agentspan.runtime.compiler.AgentCompiler; +import dev.agentspan.runtime.model.AgentConfig; +import dev.agentspan.runtime.registry.RegisteredAgent.ExposeAsTool; + +/** + * Generic registrar that drives every {@link RegisteredAgent} bean through + * the same compile → (optional auto-expose stamp) → persist pipeline on + * server startup. + * + *

Replaces feature-specific {@code @PostConstruct registerWorkflow()} + * methods that previously coupled OCG (and future sub-agents) to the + * mechanics of metadata-store writes. Adding a new server-side sub-agent + * now only requires declaring a {@code @Bean RegisteredAgent}.

+ */ +@Component +@DependsOn("registeredTaskDefsRegistrar") +public class RegisteredAgentRegistrar { + + private static final Logger log = LoggerFactory.getLogger(RegisteredAgentRegistrar.class); + + private final AgentCompiler agentCompiler; + private final MetadataDAO metadataDAO; + private final List registeredAgents; + + @Autowired + public RegisteredAgentRegistrar( + AgentCompiler agentCompiler, + MetadataDAO metadataDAO, + @Autowired(required = false) List registeredAgents) { + this.agentCompiler = agentCompiler; + this.metadataDAO = metadataDAO; + this.registeredAgents = registeredAgents != null ? registeredAgents : List.of(); + } + + @PostConstruct + public void registerAll() { + for (RegisteredAgent agent : registeredAgents) { + register(agent); + } + if (!registeredAgents.isEmpty()) { + log.info("Registered {} server-side agent(s)", registeredAgents.size()); + } + } + + private void register(RegisteredAgent agent) { + AgentConfig config = agent.agentConfig(); + WorkflowDef def = agentCompiler.compile(config); + ExposeAsTool expose = agent.autoExpose(); + if (expose != null) { + stampAutoExpose(def, expose); + } + metadataDAO.updateWorkflowDef(def); + log.info( + "Registered agent: workflow='{}'{}", + def.getName(), + expose != null ? " autoExposeAs='" + expose.toolName() + "'" : ""); + } + + private static void stampAutoExpose(WorkflowDef def, ExposeAsTool expose) { + Map metadata = + def.getMetadata() != null ? new LinkedHashMap<>(def.getMetadata()) : new LinkedHashMap<>(); + metadata.put( + AgentCompiler.AUTO_EXPOSE_AS_TOOL_METADATA_KEY, + Map.of("name", expose.toolName(), "description", expose.toolDescription())); + def.setMetadata(metadata); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java new file mode 100644 index 000000000..6b0694038 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.registry; + +import java.util.List; + +import com.netflix.conductor.common.metadata.tasks.TaskDef; + +/** + * Marker for a Spring bean that contributes Conductor {@link TaskDef} + * entries to the metadata store at startup. + * + *

Conductor's dynamic-fork dispatcher resolves tasks by name + * via the TaskDef registry. Custom system task types (e.g. {@code OCG_QUERY}) + * therefore need a matching TaskDef registered before any workflow can + * dispatch them. Beans of this type plug into + * {@link RegisteredTaskDefsRegistrar} and the registration happens + * generically — no per-feature {@code @PostConstruct}.

+ */ +public interface RegisteredTaskDefs { + + /** Task definitions this bean wants registered. */ + List taskDefs(); +} diff --git a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java new file mode 100644 index 000000000..58a336eb9 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.registry; + +import java.util.List; + +import jakarta.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.netflix.conductor.common.metadata.tasks.TaskDef; +import com.netflix.conductor.dao.MetadataDAO; + +/** + * Generic registrar that writes every {@link RegisteredTaskDefs}-contributed + * {@link TaskDef} into Conductor's metadata store on startup. + * + *

{@link RegisteredAgentRegistrar} declares an explicit dependency on + * this bean via {@code @DependsOn} so task defs are written before any + * agent workflow compiles — agent configs frequently reference task + * names whose defs must already exist.

+ */ +@Component +public class RegisteredTaskDefsRegistrar { + + private static final Logger log = LoggerFactory.getLogger(RegisteredTaskDefsRegistrar.class); + + private final MetadataDAO metadataDAO; + private final List suppliers; + + @Autowired + public RegisteredTaskDefsRegistrar( + MetadataDAO metadataDAO, @Autowired(required = false) List suppliers) { + this.metadataDAO = metadataDAO; + this.suppliers = suppliers != null ? suppliers : List.of(); + } + + @PostConstruct + public void registerAll() { + int count = 0; + for (RegisteredTaskDefs supplier : suppliers) { + for (TaskDef def : supplier.taskDefs()) { + metadataDAO.updateTaskDef(def); + count++; + } + } + if (count > 0) { + log.info("Registered {} TaskDef(s) from {} supplier(s)", count, suppliers.size()); + } + } +} diff --git a/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java b/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java new file mode 100644 index 000000000..9861ce27a --- /dev/null +++ b/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.registry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.netflix.conductor.common.metadata.workflow.WorkflowDef; +import com.netflix.conductor.dao.MetadataDAO; + +import dev.agentspan.runtime.compiler.AgentCompiler; +import dev.agentspan.runtime.model.AgentConfig; +import dev.agentspan.runtime.registry.RegisteredAgent.ExposeAsTool; + +/** + * Unit tests for {@link RegisteredAgentRegistrar}. + * + *

Pins the contract every server-side sub-agent (OCG and any future + * peer) relies on: declare a bean, the framework compiles it, stamps + * auto-expose metadata if requested, and writes it to the metadata DAO.

+ */ +class RegisteredAgentRegistrarTest { + + @Test + void compilesAndRegistersEveryRegisteredAgent() { + AgentCompiler compiler = mock(AgentCompiler.class); + MetadataDAO dao = mock(MetadataDAO.class); + when(compiler.compile(any())).thenAnswer(inv -> { + AgentConfig cfg = inv.getArgument(0); + WorkflowDef def = new WorkflowDef(); + def.setName(cfg.getName()); + return def; + }); + + RegisteredAgent a = stubAgent("alpha_agent", null); + RegisteredAgent b = stubAgent("beta_agent", null); + + new RegisteredAgentRegistrar(compiler, dao, List.of(a, b)).registerAll(); + + verify(compiler).compile(a.agentConfig()); + verify(compiler).compile(b.agentConfig()); + ArgumentCaptor captor = ArgumentCaptor.forClass(WorkflowDef.class); + verify(dao, org.mockito.Mockito.times(2)).updateWorkflowDef(captor.capture()); + assertThat(captor.getAllValues().stream().map(WorkflowDef::getName)) + .containsExactlyInAnyOrder("alpha_agent", "beta_agent"); + } + + @Test + void stampsAutoExposeMetadataWhenAgentRequestsIt() { + // The stamp is what makes a registered agent LLM-visible to other + // agents via AgentCompiler.mergeAutoExposedTools. Drift here would + // silently hide every server-side sub-agent from end-user agents. + AgentCompiler compiler = mock(AgentCompiler.class); + MetadataDAO dao = mock(MetadataDAO.class); + when(compiler.compile(any())).thenReturn(emptyDef("helper")); + + RegisteredAgent agent = stubAgent("helper", new ExposeAsTool("helper_tool", "Call when stuck.")); + + new RegisteredAgentRegistrar(compiler, dao, List.of(agent)).registerAll(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(WorkflowDef.class); + verify(dao).updateWorkflowDef(captor.capture()); + Object stamped = captor.getValue().getMetadata().get(AgentCompiler.AUTO_EXPOSE_AS_TOOL_METADATA_KEY); + assertThat(stamped).isInstanceOf(Map.class); + @SuppressWarnings("unchecked") + Map spec = (Map) stamped; + assertThat(spec).containsEntry("name", "helper_tool").containsEntry("description", "Call when stuck."); + } + + @Test + void doesNotStampWhenAutoExposeReturnsNull() { + // A registered agent that exists server-side but isn't meant to be + // LLM-visible (private helper, internal pipeline) must come back + // from the DAO without the auto-expose flag. + AgentCompiler compiler = mock(AgentCompiler.class); + MetadataDAO dao = mock(MetadataDAO.class); + when(compiler.compile(any())).thenReturn(emptyDef("internal")); + + new RegisteredAgentRegistrar(compiler, dao, List.of(stubAgent("internal", null))).registerAll(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(WorkflowDef.class); + verify(dao).updateWorkflowDef(captor.capture()); + Map metadata = captor.getValue().getMetadata(); + // metadata may be null OR present without the auto-expose key — + // either way the LLM-visibility contract isn't tripped. + if (metadata != null) { + assertThat(metadata).doesNotContainKey(AgentCompiler.AUTO_EXPOSE_AS_TOOL_METADATA_KEY); + } + } + + @Test + void emptyAgentListIsANoOp() { + AgentCompiler compiler = mock(AgentCompiler.class); + MetadataDAO dao = mock(MetadataDAO.class); + + new RegisteredAgentRegistrar(compiler, dao, null).registerAll(); + new RegisteredAgentRegistrar(compiler, dao, List.of()).registerAll(); + + // Neither compile nor write should fire — the registrar must be + // benign when no RegisteredAgent beans are present (e.g. OCG off + // and no future sub-agents declared). + verifyNoInteractions(compiler); + verifyNoInteractions(dao); + } + + // ───────────────────────────────────────────────────────────────────── + // Helpers + // ───────────────────────────────────────────────────────────────────── + + private static RegisteredAgent stubAgent(String name, ExposeAsTool expose) { + AgentConfig config = AgentConfig.builder().name(name).build(); + return new RegisteredAgent() { + @Override + public AgentConfig agentConfig() { + return config; + } + + @Override + public ExposeAsTool autoExpose() { + return expose; + } + }; + } + + private static WorkflowDef emptyDef(String name) { + WorkflowDef def = new WorkflowDef(); + def.setName(name); + def.setMetadata(new LinkedHashMap<>()); + return def; + } +} diff --git a/server/src/test/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrarTest.java b/server/src/test/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrarTest.java new file mode 100644 index 000000000..b74e06e94 --- /dev/null +++ b/server/src/test/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrarTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.registry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.netflix.conductor.common.metadata.tasks.TaskDef; +import com.netflix.conductor.dao.MetadataDAO; + +/** + * Unit tests for {@link RegisteredTaskDefsRegistrar}. + * + *

Each {@link RegisteredTaskDefs} bean's contribution must reach the + * metadata DAO unmodified. Conductor's dynamic-dispatch lookup is by + * task name; missing or mangled defs surface as + * "Cannot find task by name X" at runtime.

+ */ +class RegisteredTaskDefsRegistrarTest { + + @Test + void writesEveryContributedTaskDefToTheDao() { + MetadataDAO dao = mock(MetadataDAO.class); + RegisteredTaskDefs supplierA = () -> List.of(def("ocg_query"), def("ocg_get_entity")); + RegisteredTaskDefs supplierB = () -> List.of(def("another_tool")); + + new RegisteredTaskDefsRegistrar(dao, List.of(supplierA, supplierB)).registerAll(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(TaskDef.class); + verify(dao, times(3)).updateTaskDef(captor.capture()); + assertThat(captor.getAllValues().stream().map(TaskDef::getName)) + .containsExactlyInAnyOrder("ocg_query", "ocg_get_entity", "another_tool"); + } + + @Test + void noSuppliersMeansNoWrites() { + MetadataDAO dao = mock(MetadataDAO.class); + + new RegisteredTaskDefsRegistrar(dao, null).registerAll(); + new RegisteredTaskDefsRegistrar(dao, List.of()).registerAll(); + + verifyNoInteractions(dao); + } + + private static TaskDef def(String name) { + TaskDef d = new TaskDef(); + d.setName(name); + return d; + } +} From d24531ce07534800d960d134afa4415ec14712c0 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Tue, 9 Jun 2026 13:06:38 -0700 Subject: [PATCH 08/61] refactor(compiler): split mergeAutoExposedTools into focused helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The method was a 60-line block mashing six concerns: DAO fetch with try/catch, existing-name collection, per-workflow metadata parsing, self/duplicate guards, ToolConfig construction, and write-back. Split into: - mergeAutoExposedTools — the loop, ~15 lines, zero control-flow keywords inside the body (no continue/break). Reads as: "fetch defs, collect taken names, for each def Optional.ifPresent build, commit if anything was added." - tryBuildAgentTool — per-workflow filter chain with named guard clauses; returns Optional - safelyFetchAllWorkflowDefs — the DAO call + warn-on-failure - collectToolNames — existing-name accumulation - readAutoExposeSpec — metadata parsing into a typed AutoExposeSpec record - buildAgentTool — ToolConfig construction - appendTools — copy-on-write list mutation The two-pass "contains then add" dedup collapses into a single takenNames.add(...) check that does both, halving the guard lines. Behavior unchanged — 714 tests still green. --- .../runtime/compiler/AgentCompiler.java | 140 +++++++++++------- 1 file changed, 89 insertions(+), 51 deletions(-) diff --git a/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java b/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java index 91c26e34e..c14669df4 100644 --- a/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java +++ b/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java @@ -2476,66 +2476,104 @@ static Set collectCapabilities(AgentConfig config) { * — caller's explicit declaration wins * */ - @SuppressWarnings("unchecked") void mergeAutoExposedTools(AgentConfig config) { if (config == null || metadataDAO == null) return; - List defs; + List defs = safelyFetchAllWorkflowDefs(); + if (defs.isEmpty()) return; + + Set takenNames = collectToolNames(config); + List toAppend = new ArrayList<>(); + for (WorkflowDef def : defs) { + tryBuildAgentTool(def, config.getName(), takenNames).ifPresent(tool -> { + toAppend.add(tool); + log.info( + "Auto-exposed workflow '{}' as agent_tool '{}' on '{}'", + def.getName(), + tool.getName(), + config.getName()); + }); + } + if (!toAppend.isEmpty()) { + appendTools(config, toAppend); + } + } + + /** + * Yield an {@code agent_tool} {@link ToolConfig} for {@code def} if and only if + * {@code def} carries a well-formed auto-expose marker, is not the workflow being + * compiled, and its tool name isn't already taken. Mutates {@code takenNames} on + * success so subsequent calls in the same pass dedupe against this one too. + * + *

Guard clauses on the unhappy paths; one happy-path return at the bottom. The + * caller has no control-flow keywords — just consumes the {@link Optional}.

+ */ + private static Optional tryBuildAgentTool( + WorkflowDef def, String compileTargetName, Set takenNames) { + AutoExposeSpec spec = readAutoExposeSpec(def); + if (spec == null) return Optional.empty(); + if (def.getName().equals(compileTargetName)) return Optional.empty(); + if (!takenNames.add(spec.toolName())) return Optional.empty(); + return Optional.of(buildAgentTool(def.getName(), spec)); + } + + /** Typed view of an {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} entry. */ + private record AutoExposeSpec(String toolName, String description) {} + + /** + * Pull all latest WorkflowDefs from the DAO, swallowing any transient + * failure as a warn-and-empty. The merge is a convenience layer, not a + * correctness requirement — a failed lookup shouldn't fail the compile. + */ + private List safelyFetchAllWorkflowDefs() { try { - defs = metadataDAO.getAllWorkflowDefsLatestVersions(); + List defs = metadataDAO.getAllWorkflowDefsLatestVersions(); + return defs == null ? List.of() : defs; } catch (Exception e) { - // Defensive — a transient DAO failure shouldn't fail the compile; - // the merge is a convenience layer, not a correctness requirement. log.warn("auto-expose merge: metadataDAO lookup failed, skipping. {}", e.getMessage()); - return; + return List.of(); } - if (defs == null || defs.isEmpty()) return; + } - List tools = config.getTools(); - Set existingNames = new HashSet<>(); - if (tools != null) { - for (ToolConfig t : tools) { - if (t.getName() != null) existingNames.add(t.getName()); - } + /** Names of every tool already declared on {@code config}. */ + private static Set collectToolNames(AgentConfig config) { + if (config.getTools() == null) return new HashSet<>(); + Set names = new HashSet<>(); + for (ToolConfig t : config.getTools()) { + if (t.getName() != null) names.add(t.getName()); } + return names; + } - List merged = null; - for (WorkflowDef def : defs) { - Map md = def.getMetadata(); - if (md == null) continue; - Object spec = md.get(AUTO_EXPOSE_AS_TOOL_METADATA_KEY); - if (!(spec instanceof Map specMap)) continue; - Object nameObj = specMap.get("name"); - if (!(nameObj instanceof String toolName) || toolName.isEmpty()) continue; - - // Self-recursion guard: the workflow being compiled cannot have - // itself appended as a tool. Match on the workflow def's name - // (the Conductor registration name, e.g. "_ocg_agent"), since - // that's what config.getName() resolves to during the - // sub-agent's own compile. - if (def.getName().equals(config.getName())) continue; - - // Duplicate guard: skip names already present on the config. - if (existingNames.contains(toolName)) continue; - - Object descObj = specMap.get("description"); - String description = descObj instanceof String s ? s : ""; - - if (merged == null) { - merged = new ArrayList<>(tools != null ? tools : List.of()); - } - merged.add(ToolConfig.builder() - .name(toolName) - .toolType("agent_tool") - .description(description) - .config(Map.of("workflowName", def.getName())) - .build()); - existingNames.add(toolName); - log.info( - "Auto-exposed workflow '{}' as agent_tool '{}' on '{}'", def.getName(), toolName, config.getName()); - } - if (merged != null) { - config.setTools(merged); - } + /** + * Read the auto-expose marker off a {@link WorkflowDef}, returning null + * when the marker is absent or malformed. Type-checks the spec map and + * its {@code name} field; defaults missing description to empty string. + */ + private static AutoExposeSpec readAutoExposeSpec(WorkflowDef def) { + Map metadata = def.getMetadata(); + if (metadata == null || !(metadata.get(AUTO_EXPOSE_AS_TOOL_METADATA_KEY) instanceof Map spec)) { + return null; + } + if (!(spec.get("name") instanceof String toolName) || toolName.isEmpty()) return null; + String description = spec.get("description") instanceof String s ? s : ""; + return new AutoExposeSpec(toolName, description); + } + + /** Build the agent_tool ToolConfig the LLM tool list ends up seeing. */ + private static ToolConfig buildAgentTool(String workflowName, AutoExposeSpec spec) { + return ToolConfig.builder() + .name(spec.toolName()) + .toolType("agent_tool") + .description(spec.description()) + .config(Map.of("workflowName", workflowName)) + .build(); + } + + /** Copy-on-write the tools list with the appended entries set back on {@code config}. */ + private static void appendTools(AgentConfig config, List toAppend) { + List merged = new ArrayList<>(config.getTools() != null ? config.getTools() : List.of()); + merged.addAll(toAppend); + config.setTools(merged); } // Setters for configuration From 7d15f59b670a673a1d18251a9260518837275284 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Tue, 9 Jun 2026 13:34:41 -0700 Subject: [PATCH 09/61] feat(ocg): bake current date into OCG sub-agent system prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OCG sub-agent's LLM was hallucinating date ranges (e.g. picking 2023-10-10 as end_time when "today" is 2026-06-09), filtering out fresh data and producing thin synthesis. Anchor the LLM on a real date: - Add TODAY_PLACEHOLDER ({{TODAY}}) to OCG_SYSTEM_PROMPT - OcgAgentFactory.build() replaces it with LocalDate.now(UTC) at workflow-compile time - Prompt opens with "Today's date is (UTC)" and explicit guidance to anchor relative ranges on it and omit ranges when none are implied Refreshes on every server restart since RegisteredAgentRegistrar recompiles _ocg_agent at startup. Long-running servers will drift — fix can move to a runtime-resolved template var later if that matters. --- .../runtime/ocg/OcgAgentFactory.java | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java index 99f8659c6..4f0924a7d 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java @@ -44,11 +44,23 @@ public final class OcgAgentFactory { + "supporting citations."; /** - * System prompt for the OCG sub-agent. Supplied verbatim by the feature - * spec — explains the retrieval/aggregation split and the two-step - * pattern callers must follow. + * Marker {@link #build} replaces with the current UTC date so the LLM + * doesn't hallucinate date ranges. Computed at compile time, refreshes + * on every server restart. */ - static final String OCG_SYSTEM_PROMPT = "You are querying an OCG (Observability Context Graph). It is a RETRIEVAL\n" + static final String TODAY_PLACEHOLDER = "{{TODAY}}"; + + /** + * System prompt template for the OCG sub-agent. {@link #TODAY_PLACEHOLDER} + * is replaced with today's UTC date in {@link #build} so any + * "recent" / relative-date query gets bounded against a real anchor + * instead of whatever year the model felt like inventing. + */ + static final String OCG_SYSTEM_PROMPT = "Today's date is " + TODAY_PLACEHOLDER + " (UTC). When a user asks for\n" + + "\"recent\" / \"last week\" / any relative range, anchor on this date.\n" + + "Never invent a date range — if no range is implied by the user, omit\n" + + "start_time/end_time from the request.\n\n" + + "You are querying an OCG (Observability Context Graph). It is a RETRIEVAL\n" + "engine over a knowledge graph of entities (messages, channels, people)\n" + "linked by claims and relationships. It is NOT an aggregation engine.\n\n" + "It can answer:\n" @@ -87,11 +99,14 @@ public final class OcgAgentFactory { private OcgAgentFactory() {} public static AgentConfig build(OcgProperties props) { + String prompt = OCG_SYSTEM_PROMPT.replace( + TODAY_PLACEHOLDER, + java.time.LocalDate.now(java.time.ZoneOffset.UTC).toString()); return AgentConfig.builder() .name(AGENT_NAME) .description("Retrieval sub-agent over the Open Context Graph (OCG).") .model(props.getModel()) - .instructions(OCG_SYSTEM_PROMPT) + .instructions(prompt) .tools(buildTools()) .maxTurns(10) .build(); From 2d44cade6a4821c93d8242650e9a5b82a3d53569 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Tue, 9 Jun 2026 13:47:16 -0700 Subject: [PATCH 10/61] fix(ocg): import java.time classes instead of inline FQN The CI guard 'checkNoInlineFQN' caught the inline java.time.LocalDate.now(java.time.ZoneOffset.UTC) call I added when baking today's date into the OCG system prompt. --- .../main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java index 4f0924a7d..1b1e39aee 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java @@ -5,6 +5,8 @@ package dev.agentspan.runtime.ocg; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -100,8 +102,7 @@ private OcgAgentFactory() {} public static AgentConfig build(OcgProperties props) { String prompt = OCG_SYSTEM_PROMPT.replace( - TODAY_PLACEHOLDER, - java.time.LocalDate.now(java.time.ZoneOffset.UTC).toString()); + TODAY_PLACEHOLDER, LocalDate.now(ZoneOffset.UTC).toString()); return AgentConfig.builder() .name(AGENT_NAME) .description("Retrieval sub-agent over the Open Context Graph (OCG).") From ab2f7c06e253c9031605347ea143323d922a5250 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Tue, 9 Jun 2026 14:28:39 -0700 Subject: [PATCH 11/61] Add docs --- docs/index.md | 1 + docs/ocg-agent-flow.md | 341 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 docs/ocg-agent-flow.md diff --git a/docs/index.md b/docs/index.md index e7daf8b52..5f75ec8af 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,6 +29,7 @@ Agentspan is a durable runtime for AI agents. Execution state lives server-side, - [Deployment overview](deployment.md) - Local development, Docker, Helm, and Orkes Cloud. - [Self-hosting](self-hosting.md) - Run Agentspan in your own environment. +- [OCG Sub-Agent integration](ocg-agent-flow.md) - Set `OCG_URL` + `OCG_API_KEY` to give every user agent a built-in retrieval tool over the Open Context Graph. ## Examples diff --git a/docs/ocg-agent-flow.md b/docs/ocg-agent-flow.md new file mode 100644 index 000000000..9e2a6402e --- /dev/null +++ b/docs/ocg-agent-flow.md @@ -0,0 +1,341 @@ +# OCG Sub-Agent + +A built-in retrieval sub-agent that any user's LLM can delegate to mid-loop +when it needs context from the Open Context Graph (OCG) — Slack messages, +Jira tickets, code history, stored memories. Enabled by setting +`OCG_URL`. Disabled by leaving it unset. + +The feature is also the **first consumer of a generic +`RegisteredAgent` registry pattern**: any future server-side sub-agent +plugs in as one `@Component` without touching `AgentCompiler`, +`AgentService`, or any per-feature `@PostConstruct` boilerplate. + +--- + +## Setup — integrating OCG with AgentSpan + +OCG is fully opt-in. The integration is **two environment variables** the +AgentSpan server reads at startup: + +| Env var | Required? | What it does | +| ------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- | +| `OCG_URL` | **Yes** (to enable) | Base URL of your OCG instance, e.g. `https://dev.orkescontextgraph.io`. If unset or empty, every OCG bean stays out of the Spring context, no `_ocg_agent` workflow is registered, and no user agent gets the auto-injected `ocg_agent` tool. The feature is completely dormant. | +| `OCG_API_KEY` | Yes (if OCG requires auth) | Bearer token sent as `Authorization: Bearer ` on every OCG HTTP request. Empty means no auth header — fine for unauthenticated local OCG instances; required for the hosted dev / prod instances. | + +### Local dev + +When starting the server (via `./gradlew bootRun`, IntelliJ run config, +or `java -jar`): + +```bash +export OCG_URL=https://dev.orkescontextgraph.io +export OCG_API_KEY= +export OPENAI_API_KEY=sk-... # the OCG sub-agent also needs an LLM key +./gradlew bootRun +``` + +In IntelliJ, add the same three to your Spring Boot run configuration's +**Environment variables** field. + +### Docker / production + +Pass them through whatever your deployment system uses — docker `-e`, +Kubernetes `env`, Helm values, systemd `EnvironmentFile`, etc. They map +to Spring properties via `application.properties`: + +``` +agentspan.ocg.url=${OCG_URL:} +agentspan.ocg.api-key=${OCG_API_KEY:} +``` + +So you can alternatively pass them as Spring properties on the JVM +command line (`-Dagentspan.ocg.url=…`) or via a `SPRING_APPLICATION_JSON` +blob if your platform prefers that. + +### Verifying it's enabled + +After the server starts with `OCG_URL` set you should see these three +lines in the log: + +``` +INFO dev.agentspan.runtime.registry.RegisteredTaskDefsRegistrar — Registered 7 TaskDef(s) from 1 supplier(s) +INFO dev.agentspan.runtime.registry.RegisteredAgentRegistrar — Registered agent: workflow='_ocg_agent' autoExposeAs='ocg_agent' +INFO dev.agentspan.runtime.registry.RegisteredAgentRegistrar — Registered 1 server-side agent(s) +``` + +Two quick HTTP checks: + +```bash +# 1. The OCG sub-agent workflow is registered +curl -s http://localhost:6767/api/metadata/workflow/_ocg_agent | jq .name +# → "_ocg_agent" + +# 2. The OCG primitive TaskDefs are registered +curl -s -o /dev/null -w "%{http_code}\n" http://localhost:6767/api/metadata/taskdefs/ocg_query +# → 200 +``` + +If `OCG_URL` is unset, both endpoints return 404 — that's the disabled +state. + +### Optional tuning knobs + +| Property | Default | Effect | +| ---------------------------------- | -------------------- | ---------------------------------------------------------------------------- | +| `agentspan.ocg.model` | `openai/gpt-4o-mini` | LLM the OCG sub-agent uses internally. Override via `OCG_MODEL` env or `-Dagentspan.ocg.model=…`. | +| `agentspan.ocg.response-cap-chars` | `8192` | Per-call response truncation budget. Raise if your model context allows; lower to save tokens. | + +--- + +## 1. Architecture in one picture + +```mermaid +flowchart TB + subgraph L4["Layer 4 — Generic auto-expose (OCG-agnostic)"] + AC["AgentCompiler
.mergeAutoExposedTools
.AUTO_EXPOSE_AS_TOOL_METADATA_KEY"] + RAR["RegisteredAgentRegistrar
(picks up RegisteredAgent beans)"] + RTR["RegisteredTaskDefsRegistrar
(picks up RegisteredTaskDefs beans)"] + end + subgraph L3["Layer 3 — OCG sub-agent definition"] + ORA["OcgRegisteredAgent
@Component"] + ORT["OcgRegisteredTaskDefs
@Component"] + OAF["OcgAgentFactory
(builds the AgentConfig)"] + end + subgraph L2["Layer 2 — OCG primitive system tasks"] + OcgReq["OcgRequestTask × 7
(OCG_QUERY, OCG_GET_ENTITY, …)"] + Strats["Strategy classes:
OcgQueryOperation,
OcgGetEntityOperation, …"] + end + subgraph L1["Layer 1 — Configuration"] + Props["OcgProperties
(url, apiKey, model, responseCapChars)"] + Cond["@ConditionalOnExpression
('${agentspan.ocg.url:}'.length() > 0)"] + end + + L1 --> L2 + L1 --> L3 + L3 --> L4 + L2 -.exposes task types.-> L4 +``` + +Reading bottom-up: each layer is independent of the ones above it. +**Removing OCG entirely means deleting layers 1-3; layer 4 stays generic +and useful for any other server-side sub-agent.** + +--- + +## 2. Startup — the registry does the work + +```mermaid +sequenceDiagram + autonumber + participant SB as Spring Boot + participant OcgBeans as OcgRegisteredAgent +
OcgRegisteredTaskDefs
(@Component, gated by url) + participant TaskCfg as OcgRequestTaskConfig
(gated by url) + participant TDR as RegisteredTaskDefsRegistrar
(generic) + participant AR as RegisteredAgentRegistrar
(generic) + participant AC as AgentCompiler + participant DAO as MetadataDAO + + SB->>+OcgBeans: instantiate (URL is set) + deactivate OcgBeans + SB->>+TaskCfg: instantiate — 7 OCG_* system task beans + deactivate TaskCfg + + Note over TDR,AR: @DependsOn ensures TaskDefs run first + + SB->>+TDR: @PostConstruct + TDR->>+OcgBeans: taskDefs() + OcgBeans-->>-TDR: 7 TaskDefs
(ocg_query, ocg_get_entity, …) + TDR->>+DAO: updateTaskDef × 7 + DAO-->>-TDR: ok + deactivate TDR + + SB->>+AR: @PostConstruct + AR->>+OcgBeans: agentConfig() + autoExpose() + OcgBeans-->>-AR: AgentConfig("_ocg_agent") +
ExposeAsTool("ocg_agent", "Delegate to…") + AR->>+AC: compile(AgentConfig) + AC-->>-AR: WorkflowDef "_ocg_agent" + AR->>AR: stamp def.metadata[autoExposeAsTool]
= {name, description} + AR->>+DAO: updateWorkflowDef + DAO-->>-AR: ok + deactivate AR + Note over DAO: _ocg_agent is now dispatchable
AND auto-exposed to every
top-level user agent +``` + +The registrars know nothing about OCG. They iterate `List` +and `List` provided by Spring, run each through a +fixed pipeline (compile + stamp + persist for agents; persist for +TaskDefs), and call it a day. OCG just happens to be the one feature +providing those beans today. + +--- + +## 3. Compile-time merge — how every user agent gets `ocg_agent` + +```mermaid +sequenceDiagram + autonumber + participant U as Client + participant AS as AgentService + participant AC as AgentCompiler + participant DAO as MetadataDAO + + U->>+AS: POST /api/agent/start {agentConfig, prompt} + AS->>AS: resolveConfig() — normalize framework + AS->>+AC: compile(config) + + Note over AC: mergeAutoExposedTools(config) + AC->>+DAO: getAllWorkflowDefsLatestVersions() + DAO-->>-AC: every WorkflowDef in the store + loop for each def + AC->>AC: readAutoExposeSpec(def)
(returns null unless flagged) + alt has marker, name != config.name, not duplicate + AC->>AC: append ToolConfig{
name: spec.name
toolType: "agent_tool"
config.workflowName: def.name
} + end + end + + AC->>AC: strategy dispatch (compileSimple / compileWithTools / …)
ocg_agent → SUB_WORKFLOW handler at runtime + AC-->>-AS: WorkflowDef + AS->>+DAO: updateWorkflowDef + DAO-->>-AS: ok + AS->>+DAO: startWorkflow + DAO-->>-AS: executionId + AS-->>-U: 200 {executionId} +``` + +Guards inside the merger: + +| Guard | Why | +| ---------------- | ------------------------------------------------------------------ | +| No `MetadataDAO` | Unit tests using `new AgentCompiler()` should still work | +| Self-recursion | Re-compiling `_ocg_agent` itself won't inject itself as a tool | +| Duplicate name | A caller's explicit declaration wins | + +--- + +## 4. Runtime delegation — the nested agent dispatch + +```mermaid +sequenceDiagram + autonumber + participant MLM as Main agent LLM + participant Enrich as enrich INLINE
(JS dispatch table) + participant FORK as FORK_JOIN_DYNAMIC + participant OA as _ocg_agent
(SUB_WORKFLOW) + participant OLM as OCG sub-agent LLM + participant Enrich2 as nested enrich INLINE + participant ORT as OcgRequestTask + participant OCG as OCG service
(HTTPS) + + activate MLM + MLM->>MLM: sees ocg_agent in tool spec list
decides to delegate + MLM->>+Enrich: toolCalls=[{name:"ocg_agent",args:{...}}] + Enrich->>Enrich: agentToolCfg["ocg_agent"]
→ {workflowName:"_ocg_agent"} + Enrich->>+FORK: dynamicTasks=[{type:"SUB_WORKFLOW",
name:"_ocg_agent", ...}] + FORK->>+OA: dispatch child workflow + + loop until no more tool calls + OA->>+OLM: LLM_CHAT_COMPLETE
(OCG system prompt with today's date
+ 7 ocg_* tools) + OLM-->>-OA: toolCalls=[{name:"ocg_query",
args:{query, max_results, …}}] + OA->>+Enrich2: enrich runs again
(this workflow's dispatch table) + Enrich2->>Enrich2: ocgCfg["ocg_query"]
→ {taskType:"OCG_QUERY"} + Enrich2->>+ORT: OCG_QUERY system task + deactivate Enrich2 + ORT->>+OCG: POST /api/v1/agent/query
Authorization: Bearer + OCG-->>-ORT: raw JSON citations + ORT->>ORT: project fields, cap to responseCapChars + ORT-->>-OA: result (≤ cap) + end + + OA-->>-FORK: synthesized prose answer + FORK-->>-Enrich: child workflow output + Enrich-->>-MLM: tool result (as if ocg_agent were a function) + MLM->>MLM: continues conversation with answer
as the latest tool result + deactivate MLM +``` + +The same compiled-workflow shape (LLM → enrich → fork → join → loop) runs +at **both** levels — the outer dispatches `SUB_WORKFLOW`, the inner +dispatches `OCG_QUERY` and friends. That's because `_ocg_agent` is just +another `AgentConfig` compiled through the same `AgentCompiler.compile()` +pipeline that produced the user's agent. + +--- + +## 5. The seven OCG operations + +All endpoints sit under `${agentspan.ocg.url}/api/v1`. Each is backed by +a strategy class implementing `OcgOperation` (under +`runtime/ocg/operation/`); `OcgRequestTask` is a thin orchestrator that +delegates URL/method/body/projection to the strategy. + +| Tool name (LLM-visible) | System task type | Endpoint | Method | +| ----------------------- | --------------------- | ---------------------------------------- | -------- | +| `ocg_query` | `OCG_QUERY` | `/api/v1/agent/query` | `POST` | +| `ocg_get_entity` | `OCG_GET_ENTITY` | `/api/v1/entities/{entity_id}` | `GET` | +| `ocg_neighborhood` | `OCG_NEIGHBORHOOD` | `/api/v1/graph/neighborhood/{entity_id}` | `GET` | +| `ocg_code_history` | `OCG_CODE_HISTORY` | `/api/v1/code/history/{repo_id}` | `GET` | +| `ocg_memory_set` | `OCG_MEMORY_SET` | `/api/v1/memories` | `POST` | +| `ocg_memory_reinforce` | `OCG_MEMORY_REINFORCE`| `/api/v1/memories/{key}/reinforce` | `POST` | +| `ocg_memory_delete` | `OCG_MEMORY_DELETE` | `/api/v1/memories/{key}` | `DELETE` | + +--- + +## 6. Why `@ConditionalOnExpression` instead of `@ConditionalOnProperty` + +The OCG `@Component`s use: + +```java +@ConditionalOnExpression("'${agentspan.ocg.url:}'.length() > 0") +``` + +rather than the more obvious `@ConditionalOnProperty(name = "url")` +because Spring's default for the latter is *"present and not equal to +false"* — an empty string satisfies that and would load every OCG bean +even with `OCG_URL` unset. The expression form requires a non-empty +value, which matches the intent. + +--- + +## 7. Adding a new server-side sub-agent + +Drop one `@Component`. That's it. + +```java +@Component +@ConditionalOnExpression("'${agentspan.myfeature.url:}'.length() > 0") +@RequiredArgsConstructor +public class MyRegisteredAgent implements RegisteredAgent { + + private final MyFeatureProperties properties; + + @Override + public AgentConfig agentConfig() { + return MyAgentFactory.build(properties); + } + + @Override + public ExposeAsTool autoExpose() { + return new ExposeAsTool( + "my_agent", + "Use this when …"); + } +} +``` + +If your agent has primitive system tasks that need TaskDef entries +(most pure-LLM sub-agents won't), add one more: + +```java +@Component +@ConditionalOnExpression("'${agentspan.myfeature.url:}'.length() > 0") +public class MyRegisteredTaskDefs implements RegisteredTaskDefs { + @Override + public List taskDefs() { + return List.of(/* … */); + } +} +``` + +**No `AgentCompiler` edit. No `AgentService` edit. No +`@PostConstruct registerWorkflow()`. No per-feature service class.** The +generic registrars handle the rest. From 345ddeaa68daf7d8eee3aec3c6af658041267a62 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Wed, 10 Jun 2026 10:05:57 -0700 Subject: [PATCH 12/61] refactor(compiler): top-level-only auto-expose, constructor injection, lazy cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three concerns from the OCG branch code review: 1. mergeAutoExposedTools used to fire on every recursive compile because compileSubAgent (and the graph-structure subgraph compile, and MultiAgentCompiler's swarm inner-workflow compile) all called back into the public compile() entry. The Javadoc claimed top-level-only — the code contradicted it. Nested specialist sub-agents silently inherited ocg_agent and the DAO got re-queried per nesting level. Public compile() now runs the merge then delegates to a new package-private compileWithoutAutoExpose() that does the strategy dispatch + post-processing. The three internal recursion sites switch to the non-merging entry. Pinned by mergeRunsOnceAtTopLevelOnlyAndSkipsInternalRecursion. 2. MetadataDAO was field-injected with @Autowired(required = false), forcing AutoExposedToolsMergeTest to use ReflectionTestUtils. Switched to constructor injection with a no-arg overload that preserves existing `new AgentCompiler()` call sites; AutoExposedToolsMergeTest setUp now uses `new AgentCompiler(metadataDAO)`. 3. safelyFetchAllWorkflowDefs hit the DAO on every compile. Registered server-side agents are written at @PostConstruct and don't change at runtime, so the per-request fetch was wasted work. Added a volatile List cache with double-checked locking: successes cached for the lifetime of the bean, transient DAO failures left uncached so the next compile retries (matches the existing "merge is a convenience, not a correctness requirement" contract). Three new tests in AutoExposedToolsMergeTest pin the contracts. Each was verified failing against the broken state before landing the fix. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../runtime/compiler/AgentCompiler.java | 178 +++++++++++++----- .../runtime/compiler/MultiAgentCompiler.java | 6 +- .../compiler/AutoExposedToolsMergeTest.java | 96 +++++++++- 3 files changed, 222 insertions(+), 58 deletions(-) diff --git a/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java b/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java index c14669df4..3206b7a5e 100644 --- a/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java +++ b/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java @@ -66,8 +66,41 @@ public class AgentCompiler { * {@code new AgentCompiler()} directly leave this null, and the * auto-exposed-tool merge step becomes a no-op for them. */ - @Autowired(required = false) - private MetadataDAO metadataDAO; + private final MetadataDAO metadataDAO; + + /** + * Lazy cache of the auto-exposed tool entries the DAO returns. Populated + * on first successful {@link #autoExposedEntries()} call and never + * refreshed for the lifetime of the bean — registered server-side agents + * are written at {@code @PostConstruct} time and don't change at runtime, + * so re-querying the DAO per compile would be wasted work. + * + *

A transient DAO failure is NOT cached — the next compile retries + * the lookup so a one-shot blip doesn't permanently hide the registered + * sub-agents.

+ * + *

{@code volatile} for the double-checked-locking idiom in + * {@link #autoExposedEntries()}.

+ */ + private volatile List cachedAutoExposed; + + /** + * Default no-arg constructor for tests that don't need a {@link MetadataDAO}. + * The auto-expose merge becomes a no-op when {@code metadataDAO} is null. + */ + public AgentCompiler() { + this(null); + } + + /** + * Spring-injected constructor. {@link MetadataDAO} is optional because + * the compiler is also constructed directly by unit tests that don't + * exercise the auto-expose merge. + */ + @Autowired + public AgentCompiler(@Autowired(required = false) MetadataDAO metadataDAO) { + this.metadataDAO = metadataDAO; + } /** * Sanitizes an agent name for use as a Conductor task reference name. @@ -114,20 +147,32 @@ String getText() { } /** - * Main entry point: compile an AgentConfig into a WorkflowDef. + * Public entry point: compile a top-level {@link AgentConfig} into a + * {@link WorkflowDef}. * *

Before strategy dispatch, any workflow registered in the metadata * store with the {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} flag is * silently appended to {@code config.tools} as an {@code agent_tool}. * That's how the OCG sub-agent (and any future server-side sub-agent) - * becomes LLM-visible without per-feature injection code. Only the - * top-level compile runs this merge — nested sub-agents go through - * {@link #compileSubAgent}, which deliberately skips it so a specialist - * sub-agent isn't polluted with an unrelated retrieval tool.

+ * becomes LLM-visible without per-feature injection code.

+ * + *

Internal recursion (nested sub-agents, graph-structure sub-compiles, + * MultiAgentCompiler swarm) must go through {@link #compileWithoutAutoExpose} + * instead so a specialist sub-agent isn't polluted with an unrelated + * retrieval tool and the DAO isn't queried for every level of the tree.

*/ public WorkflowDef compile(AgentConfig config) { mergeAutoExposedTools(config); + return compileWithoutAutoExpose(config); + } + /** + * Internal compile entry that performs strategy dispatch and + * post-processing without re-running the auto-expose merge. Called from + * any place inside {@code AgentCompiler} or {@code MultiAgentCompiler} + * that needs to recurse into a sub-agent's compile. + */ + WorkflowDef compileWithoutAutoExpose(AgentConfig config) { WorkflowDef wf; // Passthrough check MUST be first — passthrough configs have null model. @@ -1057,8 +1102,10 @@ WorkflowTask compileSubAgent( task.getSubWorkflowParam().setName(sub.getName()); task.setInputParameters(inputs); } else { - // Compile inline - WorkflowDef subWf = compile(sub); + // Compile inline. ``compileWithoutAutoExpose`` (not ``compile``) so + // the auto-expose merge stays a top-level-only concern — nested + // sub-agents must not inherit unrelated server-registered tools. + WorkflowDef subWf = compileWithoutAutoExpose(sub); task.setType("SUB_WORKFLOW"); task.setName(sub.getName()); task.setSubWorkflowParam(new SubWorkflowParams()); @@ -1859,8 +1906,10 @@ private SubgraphNodeResult buildSubgraphNodeTasks( List defaultTasks = new ArrayList<>(); if (subAgent != null) { - // Compile the subgraph into a WorkflowDef - WorkflowDef subWf = compile(subAgent); + // Compile the subgraph into a WorkflowDef. ``compileWithoutAutoExpose`` + // because this is internal recursion — only the top-level compile + // entry runs the auto-expose merge. + WorkflowDef subWf = compileWithoutAutoExpose(subAgent); String subRef = allocRef(usedRefs, "_sg_sub_" + nodeName); WorkflowTask subTask = new WorkflowTask(); @@ -2475,63 +2524,94 @@ static Set collectCapabilities(AgentConfig config) { *
  • A tool with that name is already declared on the config * — caller's explicit declaration wins
  • * + * + *

    Auto-exposed entries are sourced from {@link #autoExposedEntries()}, + * which lazily caches the DAO result for the lifetime of the bean. Each + * call to this method only does the cheap per-config filtering + * (self-skip + name-dedupe) against the cached entry list.

    */ void mergeAutoExposedTools(AgentConfig config) { - if (config == null || metadataDAO == null) return; - List defs = safelyFetchAllWorkflowDefs(); - if (defs.isEmpty()) return; + if (config == null) return; + List entries = autoExposedEntries(); + if (entries.isEmpty()) return; Set takenNames = collectToolNames(config); List toAppend = new ArrayList<>(); - for (WorkflowDef def : defs) { - tryBuildAgentTool(def, config.getName(), takenNames).ifPresent(tool -> { - toAppend.add(tool); - log.info( - "Auto-exposed workflow '{}' as agent_tool '{}' on '{}'", - def.getName(), - tool.getName(), - config.getName()); - }); + for (AutoExposedEntry entry : entries) { + if (entry.workflowName().equals(config.getName())) continue; // self-recursion guard + if (!takenNames.add(entry.tool().getName())) continue; // caller's declaration wins + toAppend.add(entry.tool()); + log.info( + "Auto-exposed workflow '{}' as agent_tool '{}' on '{}'", + entry.workflowName(), + entry.tool().getName(), + config.getName()); } if (!toAppend.isEmpty()) { appendTools(config, toAppend); } } + /** Typed view of an {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} entry. */ + private record AutoExposeSpec(String toolName, String description) {} + + /** + * Cached pairing of source workflow name + pre-built {@code agent_tool} + * {@link ToolConfig}. We carry the workflow name alongside the tool so + * the per-compile self-recursion guard (a workflow can't auto-expose + * itself as a tool on its own compile) stays correct without re-reading + * the {@link WorkflowDef} metadata. + */ + private record AutoExposedEntry(String workflowName, ToolConfig tool) {} + /** - * Yield an {@code agent_tool} {@link ToolConfig} for {@code def} if and only if - * {@code def} carries a well-formed auto-expose marker, is not the workflow being - * compiled, and its tool name isn't already taken. Mutates {@code takenNames} on - * success so subsequent calls in the same pass dedupe against this one too. + * Lazily build and cache the auto-exposed tool entries. Successful + * results are cached for the lifetime of the bean — registered + * server-side agents are written at server {@code @PostConstruct} and + * don't change at runtime. * - *

    Guard clauses on the unhappy paths; one happy-path return at the bottom. The - * caller has no control-flow keywords — just consumes the {@link Optional}.

    + *

    A transient DAO failure returns an empty list without + * caching, so the next compile retries the lookup. The merge is a + * convenience layer, not a correctness requirement — a failed lookup + * shouldn't fail the compile.

    */ - private static Optional tryBuildAgentTool( - WorkflowDef def, String compileTargetName, Set takenNames) { - AutoExposeSpec spec = readAutoExposeSpec(def); - if (spec == null) return Optional.empty(); - if (def.getName().equals(compileTargetName)) return Optional.empty(); - if (!takenNames.add(spec.toolName())) return Optional.empty(); - return Optional.of(buildAgentTool(def.getName(), spec)); + private List autoExposedEntries() { + List snapshot = cachedAutoExposed; + if (snapshot != null) return snapshot; + if (metadataDAO == null) { + cachedAutoExposed = List.of(); + return cachedAutoExposed; + } + synchronized (this) { + if (cachedAutoExposed != null) return cachedAutoExposed; + try { + List defs = metadataDAO.getAllWorkflowDefsLatestVersions(); + List built = buildEntries(defs == null ? List.of() : defs); + cachedAutoExposed = built; + log.debug("auto-expose merge: fetched {} workflow def(s); cached {} auto-exposed entry(ies)", + defs == null ? 0 : defs.size(), built.size()); + return built; + } catch (Exception e) { + // NOT cached — let the next compile retry the DAO. + log.warn("auto-expose merge: metadataDAO lookup failed; will retry on next compile. {}", + e.getMessage()); + return List.of(); + } + } } - /** Typed view of an {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} entry. */ - private record AutoExposeSpec(String toolName, String description) {} - /** - * Pull all latest WorkflowDefs from the DAO, swallowing any transient - * failure as a warn-and-empty. The merge is a convenience layer, not a - * correctness requirement — a failed lookup shouldn't fail the compile. + * Project the DAO's full workflow-def list down to the auto-exposed + * subset, building the {@link ToolConfig} once per workflow. */ - private List safelyFetchAllWorkflowDefs() { - try { - List defs = metadataDAO.getAllWorkflowDefsLatestVersions(); - return defs == null ? List.of() : defs; - } catch (Exception e) { - log.warn("auto-expose merge: metadataDAO lookup failed, skipping. {}", e.getMessage()); - return List.of(); + private static List buildEntries(List defs) { + List built = new ArrayList<>(); + for (WorkflowDef def : defs) { + AutoExposeSpec spec = readAutoExposeSpec(def); + if (spec == null) continue; + built.add(new AutoExposedEntry(def.getName(), buildAgentTool(def.getName(), spec))); } + return built; } /** Names of every tool already declared on {@code config}. */ diff --git a/server/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java b/server/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java index 5b6b93c39..138ce6382 100644 --- a/server/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java +++ b/server/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java @@ -1435,8 +1435,10 @@ private WorkflowDef compileSwarmAgentWorkflowWithSubAgents(AgentConfig agent, Li String transferLlmRef = agent.getName() + "_transfer_llm"; String checkTransferRef = agent.getName() + "_check_transfer"; - // 1. Compile the agent normally to preserve its multi-agent strategy - WorkflowDef innerWf = agentCompiler.compile(agent); + // 1. Compile the agent normally to preserve its multi-agent strategy. + // ``compileWithoutAutoExpose`` because this is internal recursion; + // the auto-expose merge only runs at the top-level public entry. + WorkflowDef innerWf = agentCompiler.compileWithoutAutoExpose(agent); // Inner agent as SUB_WORKFLOW WorkflowTask innerTask = new WorkflowTask(); diff --git a/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java b/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java index 2d4a7b721..afa94c255 100644 --- a/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java +++ b/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java @@ -7,6 +7,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.ArrayList; @@ -16,7 +18,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.test.util.ReflectionTestUtils; import com.netflix.conductor.common.metadata.workflow.WorkflowDef; import com.netflix.conductor.dao.MetadataDAO; @@ -40,9 +41,8 @@ class AutoExposedToolsMergeTest { @BeforeEach void setUp() { - compiler = new AgentCompiler(); metadataDAO = mock(MetadataDAO.class); - ReflectionTestUtils.setField(compiler, "metadataDAO", metadataDAO); + compiler = new AgentCompiler(metadataDAO); } @Test @@ -134,10 +134,10 @@ void doesNotDuplicateWhenAToolWithThatNameAlreadyExists() { @Test void noOpWhenMetadataDaoIsAbsent() { - // Tests that construct AgentCompiler with `new AgentCompiler()` - // (i.e. no Spring DI) must continue to work. The merger short- - // circuits cleanly when metadataDAO is null instead of NPE-ing. - AgentCompiler noDao = new AgentCompiler(); // no setField — metadataDAO stays null + // Tests that construct AgentCompiler without a metadataDAO must + // continue to work. The no-arg constructor exists for this path; + // the merger short-circuits cleanly instead of NPE-ing. + AgentCompiler noDao = new AgentCompiler(); // null metadataDAO AgentConfig config = AgentConfig.builder() .name("user_agent") @@ -170,6 +170,88 @@ void appendsMultipleFlaggedWorkflowsInDaoOrder() { assertThat(config.getTools().get(1).getName()).isEqualTo("beta_agent"); } + @Test + void mergeRunsOnceAtTopLevelOnlyAndSkipsInternalRecursion() { + // Pinning the contract that the public ``compile()`` is the only entry + // that runs the auto-expose merge. Internal recursion (compileSubAgent, + // graph-structure subgraph compile, MultiAgentCompiler swarm) must go + // through the non-merging entry so nested specialist sub-agents don't + // silently pick up unrelated server-side tools. + WorkflowDef flagged = wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "Use when stuck."); + when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(flagged)); + + AgentConfig inner = AgentConfig.builder() + .name("inner_specialist") + .model("openai/gpt-4o-mini") + .build(); + + // compileSubAgent is the entry that nested compilation goes through. + // It must NOT mutate ``inner.tools`` with the auto-exposed entry. + compiler.compileSubAgent( + inner, "inner_ref", "${workflow.input.prompt}", "${workflow.input.media}", null); + + boolean innerHasAutoExposed = inner.getTools() != null + && inner.getTools().stream().anyMatch(t -> "helper_agent".equals(t.getName())); + assertThat(innerHasAutoExposed) + .as("nested sub-agent must NOT have the auto-exposed tool merged into its tool list") + .isFalse(); + } + + @Test + void daoQueriedOnlyOnceAcrossMultipleMerges() { + // Lazy cache: registered server-side agents are written at @PostConstruct + // and don't change at runtime, so the per-compile DAO fetch is wasted + // work after the first one. Pin the contract so anyone removing the + // cache trips this test. + WorkflowDef flagged = wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "x"); + when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(flagged)); + + AgentConfig a = AgentConfig.builder() + .name("agent_a") + .tools(new ArrayList<>()) + .build(); + AgentConfig b = AgentConfig.builder() + .name("agent_b") + .tools(new ArrayList<>()) + .build(); + + compiler.mergeAutoExposedTools(a); + compiler.mergeAutoExposedTools(b); + + verify(metadataDAO, times(1)).getAllWorkflowDefsLatestVersions(); + // Both configs still received the merge from the cached result. + assertThat(a.getTools()).extracting(ToolConfig::getName).contains("helper_agent"); + assertThat(b.getTools()).extracting(ToolConfig::getName).contains("helper_agent"); + } + + @Test + void daoFailureIsNotCachedAndIsRetriedOnNextMerge() { + // Caching the failure path would turn a transient blip into a + // permanent silent loss of the merge. Stub: first call throws, second + // call returns a flagged def. The second merge must pick it up. + when(metadataDAO.getAllWorkflowDefsLatestVersions()) + .thenThrow(new RuntimeException("transient DAO failure")) + .thenReturn(List.of(wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "x"))); + + AgentConfig first = AgentConfig.builder() + .name("first") + .tools(new ArrayList<>()) + .build(); + AgentConfig second = AgentConfig.builder() + .name("second") + .tools(new ArrayList<>()) + .build(); + + compiler.mergeAutoExposedTools(first); + compiler.mergeAutoExposedTools(second); + + // First merge happened during the transient failure → no auto-expose. + assertThat(first.getTools()).extracting(ToolConfig::getName).doesNotContain("helper_agent"); + // Second merge re-queried the DAO and picked the entry up. + assertThat(second.getTools()).extracting(ToolConfig::getName).contains("helper_agent"); + verify(metadataDAO, times(2)).getAllWorkflowDefsLatestVersions(); + } + // ───────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────── From 52006f1b6dcbbcaa73f4d036ee8fdcccd7c1b1ef Mon Sep 17 00:00:00 2001 From: nicholascole Date: Wed, 10 Jun 2026 10:06:18 -0700 Subject: [PATCH 13/61] refactor(ocg): preserve interrupt flag, tighten throws, pin body shapes Smaller review items from the OCG branch: - OcgRequestTask.start used to catch Exception, which swallowed InterruptedException without re-flagging the current thread. A task cancelled mid-http.send would appear to "fail" silently and Conductor's executor would never observe the cancellation. Now catches InterruptedException separately and calls Thread.currentThread().interrupt() before failing. The remaining catch is IOException | RuntimeException so Error still propagates. - OcgRequestTask.send/complete declared `throws Exception`; tightened to the actual checked exceptions. OcgOperation.build is also tightened from `throws Exception` to `throws IOException` (JsonProcessingException extends IOException, so the postJson-using operations still compile). - Two missing OcgRequestTaskTest cases for the memory_set and memory_reinforce body shapes: memory_set must strip the server-side __agentspan_ctx__ glob before forwarding; memory_reinforce must only forward the four picked fields (no key, no __agentspan_ctx__, no rogue fields the LLM might attach). Tests read the actual HttpRequest body via a Flow.Subscriber helper. - OcgAgentFactoryTest now pins the {{TODAY}} substitution and the absence of the literal placeholder in the rendered prompt. Each new test was verified failing against an intentionally broken state before landing. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../agentspan/runtime/ocg/OcgRequestTask.java | 14 +- .../OcgMemoryReinforceOperation.java | 3 +- .../ocg/operation/OcgMemorySetOperation.java | 3 +- .../runtime/ocg/operation/OcgOperation.java | 8 +- .../ocg/operation/OcgQueryOperation.java | 3 +- .../runtime/ocg/OcgAgentFactoryTest.java | 13 ++ .../runtime/ocg/OcgRequestTaskTest.java | 155 ++++++++++++++++++ 7 files changed, 192 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java index a198ebcee..db79a3c78 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java @@ -5,6 +5,7 @@ package dev.agentspan.runtime.ocg; +import java.io.IOException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; @@ -76,18 +77,25 @@ public void start(WorkflowModel workflow, TaskModel task, WorkflowExecutor execu return; } complete(task, response.body()); - } catch (Exception e) { + } catch (InterruptedException e) { + // Re-flag the interrupt on the current thread so Conductor's + // executor (and anyone else up the stack) can observe the + // cancellation. Without this, a cancelled task would silently + // appear to "fail" without the interrupt ever propagating. + Thread.currentThread().interrupt(); + fail(task, "OCG " + operation.name() + " was interrupted"); + } catch (IOException | RuntimeException e) { fail(task, "OCG " + operation.name() + " failed: " + e.getMessage()); } } - private HttpResponse send(TaskModel task) throws Exception { + private HttpResponse send(TaskModel task) throws IOException, InterruptedException { Map input = task.getInputData() != null ? task.getInputData() : Map.of(); HttpRequest request = operation.build(properties, input); return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); } - private void complete(TaskModel task, String body) throws Exception { + private void complete(TaskModel task, String body) throws IOException { Object parsed = OcgInputs.parseJsonLenient(body); Object projected = operation.project(parsed); String serialized = OcgInputs.writeJson(projected); diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java index 82003e57d..0daf0c1ee 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java @@ -5,6 +5,7 @@ package dev.agentspan.runtime.ocg.operation; +import java.io.IOException; import java.net.URI; import java.net.http.HttpRequest; import java.util.Map; @@ -28,7 +29,7 @@ public String name() { } @Override - public HttpRequest build(OcgProperties properties, Map input) throws Exception { + public HttpRequest build(OcgProperties properties, Map input) throws IOException { String key = OcgInputs.required(input, "key"); Map body = OcgInputs.pick(input, "agent", "user", "confidence_boost", "source_ref"); URI uri = OcgUri.forApi(properties) diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java index 1fe1de75a..8e82a2447 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java @@ -5,6 +5,7 @@ package dev.agentspan.runtime.ocg.operation; +import java.io.IOException; import java.net.URI; import java.net.http.HttpRequest; import java.util.LinkedHashMap; @@ -36,7 +37,7 @@ public String name() { } @Override - public HttpRequest build(OcgProperties properties, Map input) throws Exception { + public HttpRequest build(OcgProperties properties, Map input) throws IOException { Map body = new LinkedHashMap<>(input); body.remove("__agentspan_ctx__"); URI uri = OcgUri.forApi(properties).pathSegment("memories").build().toUri(); diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java index a63822675..61ad4a186 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java @@ -5,6 +5,7 @@ package dev.agentspan.runtime.ocg.operation; +import java.io.IOException; import java.net.http.HttpRequest; import java.util.Map; @@ -31,8 +32,13 @@ public interface OcgOperation { * Build the HTTP request for this operation. Implementations should * use {@link OcgRequest} and {@link OcgUri} so authentication headers * and base-URL handling stay consistent across endpoints. + * + *

    {@link IOException} (which includes Jackson's + * {@code JsonProcessingException}) is the only checked exception + * implementations may throw — request building is purely an I/O-shape + * concern and should not surface arbitrary checked exceptions.

    */ - HttpRequest build(OcgProperties properties, Map input) throws Exception; + HttpRequest build(OcgProperties properties, Map input) throws IOException; /** * Project the parsed JSON response down to the fields the LLM needs. diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java index f18a1aed4..862cd0ab5 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java @@ -5,6 +5,7 @@ package dev.agentspan.runtime.ocg.operation; +import java.io.IOException; import java.net.URI; import java.net.http.HttpRequest; import java.util.ArrayList; @@ -31,7 +32,7 @@ public String name() { } @Override - public HttpRequest build(OcgProperties properties, Map input) throws Exception { + public HttpRequest build(OcgProperties properties, Map input) throws IOException { Map body = OcgInputs.pick(input, "query", "max_results", "traversal_level", "start_time", "end_time"); URI uri = diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java index 571a0145a..06568f3a5 100644 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java @@ -7,6 +7,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.List; import org.junit.jupiter.api.Test; @@ -71,6 +73,17 @@ void exposesAllSevenOcgToolsWithMatchingToolTypes() { "ocg_memory_delete"); } + @Test + void systemPromptHasTodayUtcDateSubstituted() { + // The {{TODAY}} placeholder is the anchor for "recent" / "last week" + // style queries. If a refactor silently drops the .replace() call, the + // LLM gets a literal "{{TODAY}}" and starts inventing dates again — + // exactly the failure mode the placeholder was added to prevent. + String prompt = OcgAgentFactory.build(props()).getInstructions().toString(); + assertThat(prompt).contains(LocalDate.now(ZoneOffset.UTC).toString()); + assertThat(prompt).doesNotContain("{{TODAY}}"); + } + @Test void queryToolDeclaresQueryAsRequiredInput() { ToolConfig queryTool = OcgAgentFactory.build(props()).getTools().stream() diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java index aba36e5d0..eeffe0deb 100644 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java @@ -8,6 +8,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -16,15 +17,24 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Flow; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.conductor.model.TaskModel; import dev.agentspan.runtime.ocg.operation.OcgGetEntityOperation; import dev.agentspan.runtime.ocg.operation.OcgMemoryDeleteOperation; +import dev.agentspan.runtime.ocg.operation.OcgMemoryReinforceOperation; +import dev.agentspan.runtime.ocg.operation.OcgMemorySetOperation; import dev.agentspan.runtime.ocg.operation.OcgNeighborhoodOperation; import dev.agentspan.runtime.ocg.operation.OcgQueryOperation; @@ -229,4 +239,149 @@ void disabledPropertiesYieldsFailedTask() throws Exception { // Importantly: no HTTP call attempted when disabled. verifyNoInteractions(http); } + + @Test + void memorySetOperationPostsToMemoriesEndpointAndStripsAgentspanCtx() throws Exception { + HttpClient http = mock(HttpClient.class); + stubSend(http, stub(200, "{\"memory_id\":\"m1\"}")); + OcgRequestTask task = new OcgRequestTask(new OcgMemorySetOperation(), props("http://ocg.local"), http); + + Map input = new LinkedHashMap<>(); + input.put("key", "k1"); + input.put("agent", "agent:foo"); + input.put("user", "user:bar"); + input.put("string_value", "remember me"); + input.put("description", "test memory"); + // Server-side execution-token plumbing. Must NEVER be forwarded to OCG — + // it's an internal credential glob the agent framework rides on workflow + // inputs, not user-facing data. + input.put("__agentspan_ctx__", "execution-token-xyz"); + + TaskModel t = taskWith(input); + task.start(null, t, null); + + ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); + verify(http).send(req.capture(), any()); + assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/api/v1/memories"); + assertThat(req.getValue().method()).isEqualTo("POST"); + + @SuppressWarnings("unchecked") + Map body = new ObjectMapper().readValue(bodyOf(req.getValue()), Map.class); + assertThat(body) + .containsEntry("key", "k1") + .containsEntry("agent", "agent:foo") + .containsEntry("user", "user:bar") + .containsEntry("string_value", "remember me") + .containsEntry("description", "test memory") + // The whole reason this test exists. + .doesNotContainKey("__agentspan_ctx__"); + assertThat(t.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); + } + + @Test + void memoryReinforceOperationPostsToReinforceEndpointWithFilteredBody() throws Exception { + HttpClient http = mock(HttpClient.class); + stubSend(http, stub(200, "{\"reinforced\":true}")); + OcgRequestTask task = + new OcgRequestTask(new OcgMemoryReinforceOperation(), props("http://ocg.local"), http); + + Map input = new LinkedHashMap<>(); + input.put("key", "k1"); + input.put("agent", "agent:foo"); + input.put("user", "user:bar"); + input.put("confidence_boost", 0.05); + input.put("source_ref", "msg:42"); + // Must NOT leak. + input.put("__agentspan_ctx__", "execution-token-xyz"); + // Must NOT be projected into the body either — pick() only takes the + // explicitly listed fields. + input.put("rogue_field", "nope"); + + TaskModel t = taskWith(input); + task.start(null, t, null); + + ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); + verify(http).send(req.capture(), any()); + // key is part of the URL; the body must be the picked subset. + assertThat(req.getValue().uri().toString()) + .isEqualTo("http://ocg.local/api/v1/memories/k1/reinforce"); + assertThat(req.getValue().method()).isEqualTo("POST"); + + @SuppressWarnings("unchecked") + Map body = new ObjectMapper().readValue(bodyOf(req.getValue()), Map.class); + // Allow-list: only the four picked fields are forwarded. Pinning the + // exact key set so a future widening of pick() can't silently leak + // server-side plumbing. + assertThat(body).containsOnlyKeys("agent", "user", "confidence_boost", "source_ref"); + assertThat(body) + .containsEntry("agent", "agent:foo") + .containsEntry("user", "user:bar") + .containsEntry("source_ref", "msg:42"); + } + + @Test + void interruptDuringHttpSendRestoresInterruptFlag() throws Exception { + HttpClient http = mock(HttpClient.class); + // HttpClient.send declares ``throws IOException, InterruptedException`` — + // doThrow on the checked InterruptedException is the canonical mocking path. + doThrow(new InterruptedException("cancelled")) + .when(http) + .send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)); + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props("http://ocg.local"), http); + + TaskModel t = taskWith(Map.of("query", "x")); + // Clear any flag accidentally left on by an earlier test on the shared + // test thread; we want to observe ONLY the flag the task itself sets. + Thread.interrupted(); + + task.start(null, t, null); + + // ``Thread.interrupted()`` both reads AND clears the flag — perfect for + // a one-shot assertion. The contract under test: catching + // InterruptedException must re-flag the current thread so Conductor's + // executor (and anyone else up the stack) can observe the cancellation. + boolean restored = Thread.interrupted(); + assertThat(restored) + .as("InterruptedException must be re-flagged on the current thread") + .isTrue(); + assertThat(t.getStatus()).isEqualTo(TaskModel.Status.FAILED); + assertThat(t.getReasonForIncompletion()).containsIgnoringCase("interrupt"); + } + + /** + * Drain an {@link HttpRequest}'s body publisher to a String. The JDK's + * {@code BodyPublishers.ofString} delivers synchronously on a single + * {@code onNext}, but we still complete a {@link CompletableFuture} on + * {@code onComplete} and wait briefly so the helper is safe against any + * future publisher variant. + */ + private static String bodyOf(HttpRequest req) throws Exception { + if (req.bodyPublisher().isEmpty()) return ""; + HttpRequest.BodyPublisher pub = req.bodyPublisher().get(); + CompletableFuture done = new CompletableFuture<>(); + pub.subscribe(new Flow.Subscriber<>() { + final StringBuilder sb = new StringBuilder(); + + @Override + public void onSubscribe(Flow.Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(ByteBuffer item) { + sb.append(StandardCharsets.UTF_8.decode(item)); + } + + @Override + public void onError(Throwable t) { + done.completeExceptionally(t); + } + + @Override + public void onComplete() { + done.complete(sb.toString()); + } + }); + return done.get(5, TimeUnit.SECONDS); + } } From 879851f859b331c58237c2b7cf5ba8fc04e9d0cb Mon Sep 17 00:00:00 2001 From: nicholascole Date: Wed, 10 Jun 2026 11:08:38 -0700 Subject: [PATCH 14/61] fix(compiler): stop registrar from snapshotting empty auto-expose cache at boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RegisteredAgentRegistrar.register()` was calling `agentCompiler.compile()`, which triggers the auto-expose merger's lazy DAO scan. During the registrar's own `@PostConstruct` loop the to-be-registered agent isn't yet in the DAO, so the merger cached an empty list and froze it for the bean's lifetime — every user compile post-startup saw no auto-exposed tools and the OCG sub-agent was silently invisible to every LLM. End-to-end smoke against a live server confirmed the bug, then confirmed the fix. - Promote `AgentCompiler.compileWithoutAutoExpose` to public; registrar now calls it so bootstrap never touches the merger cache. - Extract `AutoExposedToolsMerger` (was ~140 lines inside `AgentCompiler`) as its own `@Component`. AgentCompiler keeps a thin `mergeAutoExposedTools` delegate for the existing test API. - Add `RegisteredAgentBootstrapTest` — stateful in-memory `MetadataDAO` exercises register → user-merge ordering. Verified failing on pre-fix code, passing post-fix. - Update `RegisteredAgentRegistrarTest` mocks to track the new entry point. - Drop duplicate `agentspan.ocg.response-cap-chars` default from `application.properties` (already in `OcgProperties`). - Simplify `OcgMemoryDeleteOperation.addQueryParamIfPresent`: drop the redundant `StringUtils.defaultString` wrapping. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../runtime/compiler/AgentCompiler.java | 240 ++++-------------- .../compiler/AutoExposedToolsMerger.java | 205 +++++++++++++++ .../operation/OcgMemoryDeleteOperation.java | 3 +- .../registry/RegisteredAgentRegistrar.java | 10 +- .../src/main/resources/application.properties | 3 +- .../RegisteredAgentBootstrapTest.java | 110 ++++++++ .../RegisteredAgentRegistrarTest.java | 10 +- 7 files changed, 386 insertions(+), 195 deletions(-) create mode 100644 server/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java create mode 100644 server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java diff --git a/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java b/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java index 3206b7a5e..9e7be1dc7 100644 --- a/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java +++ b/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java @@ -44,17 +44,11 @@ public class AgentCompiler { /** * Metadata key on a {@link WorkflowDef} that marks the workflow as one * that should be silently appended to every top-level agent's tool list - * at compile time. The value is a {@code Map} with at - * least {@code name} and {@code description}; both are surfaced to the - * agent's LLM via the {@code agent_tool} routing. - * - *

    This is the single registration mechanism for "server-side - * capability the user's LLM can call." A new sub-agent only has to: - * (a) compile and register its WorkflowDef via {@link MetadataDAO}, - * (b) stamp this metadata key. {@link #compile} picks it up generically - * — no per-feature injection class required.

    + * at compile time. Re-exported from {@link AutoExposedToolsMerger} so + * callers (registrar, tests) that previously imported it from this class + * keep compiling without churn. */ - public static final String AUTO_EXPOSE_AS_TOOL_METADATA_KEY = "agentspan.autoExposeAsTool"; + public static final String AUTO_EXPOSE_AS_TOOL_METADATA_KEY = AutoExposedToolsMerger.AUTO_EXPOSE_AS_TOOL_METADATA_KEY; private int timeoutSeconds = 0; private int llmRetryCount = 3; @@ -62,44 +56,38 @@ public class AgentCompiler { private int contextMaxValueSizeBytes = 4096; /** - * Optional — only injected at runtime. Tests that construct - * {@code new AgentCompiler()} directly leave this null, and the - * auto-exposed-tool merge step becomes a no-op for them. + * Owns the DAO scan, cache, and per-config merge of auto-exposed + * server-registered agents. Pulled out so {@code AgentCompiler} stays + * focused on workflow compilation and the merge lifecycle (cache, DAO + * failure handling, self-recursion guard) lives in one place. */ - private final MetadataDAO metadataDAO; + private final AutoExposedToolsMerger autoExposedMerger; /** - * Lazy cache of the auto-exposed tool entries the DAO returns. Populated - * on first successful {@link #autoExposedEntries()} call and never - * refreshed for the lifetime of the bean — registered server-side agents - * are written at {@code @PostConstruct} time and don't change at runtime, - * so re-querying the DAO per compile would be wasted work. - * - *

    A transient DAO failure is NOT cached — the next compile retries - * the lookup so a one-shot blip doesn't permanently hide the registered - * sub-agents.

    - * - *

    {@code volatile} for the double-checked-locking idiom in - * {@link #autoExposedEntries()}.

    + * Default no-arg constructor for tests that don't need a {@link MetadataDAO}. + * The auto-expose merge becomes a no-op. */ - private volatile List cachedAutoExposed; + public AgentCompiler() { + this(AutoExposedToolsMerger.disabled()); + } /** - * Default no-arg constructor for tests that don't need a {@link MetadataDAO}. - * The auto-expose merge becomes a no-op when {@code metadataDAO} is null. + * Convenience constructor for tests that want to exercise the merge + * against a stubbed {@link MetadataDAO} without wiring up an + * {@link AutoExposedToolsMerger} explicitly. */ - public AgentCompiler() { - this(null); + public AgentCompiler(MetadataDAO metadataDAO) { + this(new AutoExposedToolsMerger(metadataDAO)); } /** - * Spring-injected constructor. {@link MetadataDAO} is optional because - * the compiler is also constructed directly by unit tests that don't - * exercise the auto-expose merge. + * Spring-injected constructor. The merger is itself a {@code @Component} + * that takes an optional {@link MetadataDAO}, so this is the path the + * runtime uses. */ @Autowired - public AgentCompiler(@Autowired(required = false) MetadataDAO metadataDAO) { - this.metadataDAO = metadataDAO; + public AgentCompiler(AutoExposedToolsMerger autoExposedMerger) { + this.autoExposedMerger = autoExposedMerger; } /** @@ -162,17 +150,40 @@ String getText() { * retrieval tool and the DAO isn't queried for every level of the tree.

    */ public WorkflowDef compile(AgentConfig config) { - mergeAutoExposedTools(config); + autoExposedMerger.merge(config); return compileWithoutAutoExpose(config); } /** - * Internal compile entry that performs strategy dispatch and - * post-processing without re-running the auto-expose merge. Called from - * any place inside {@code AgentCompiler} or {@code MultiAgentCompiler} - * that needs to recurse into a sub-agent's compile. + * Thin delegate so existing tests calling {@code compiler.mergeAutoExposedTools(config)} + * continue to compile. New code should go through the injected + * {@link AutoExposedToolsMerger} directly. */ - WorkflowDef compileWithoutAutoExpose(AgentConfig config) { + void mergeAutoExposedTools(AgentConfig config) { + autoExposedMerger.merge(config); + } + + /** + * Compile entry that performs strategy dispatch and post-processing + * without running the auto-expose merge. + * + *

    Two callers:

    + *
      + *
    • Internal recursion ({@link #compileSubAgent}, graph subgraph + * compile, {@code MultiAgentCompiler}) — nested specialist agents + * must not inherit unrelated server-side tools.
    • + *
    • {@link dev.agentspan.runtime.registry.RegisteredAgentRegistrar} + * at {@code @PostConstruct} time — registered server agents don't + * need other registered agents auto-exposed to them, AND skipping + * the merge here is what keeps the merger's lazy cache from + * snapshotting an empty list during the bootstrap loop. Letting + * the registrar trigger the merge before its own + * {@code dao.updateWorkflowDef} write would freeze the cache to + * an empty result for the life of the bean, silently hiding every + * server-registered agent from subsequent user compiles.
    • + *
    + */ + public WorkflowDef compileWithoutAutoExpose(AgentConfig config) { WorkflowDef wf; // Passthrough check MUST be first — passthrough configs have null model. @@ -2511,151 +2522,6 @@ static Set collectCapabilities(AgentConfig config) { return caps; } - /** - * Append every {@link MetadataDAO}-registered workflow that carries the - * {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} marker to {@code config.tools} - * as an {@code agent_tool}. - * - *

    Mutates {@code config} in place. Skips when:

    - *
      - *
    • {@link #metadataDAO} is absent (unit-test path)
    • - *
    • The workflow being compiled IS the auto-exposed one - * — no self-recursion
    • - *
    • A tool with that name is already declared on the config - * — caller's explicit declaration wins
    • - *
    - * - *

    Auto-exposed entries are sourced from {@link #autoExposedEntries()}, - * which lazily caches the DAO result for the lifetime of the bean. Each - * call to this method only does the cheap per-config filtering - * (self-skip + name-dedupe) against the cached entry list.

    - */ - void mergeAutoExposedTools(AgentConfig config) { - if (config == null) return; - List entries = autoExposedEntries(); - if (entries.isEmpty()) return; - - Set takenNames = collectToolNames(config); - List toAppend = new ArrayList<>(); - for (AutoExposedEntry entry : entries) { - if (entry.workflowName().equals(config.getName())) continue; // self-recursion guard - if (!takenNames.add(entry.tool().getName())) continue; // caller's declaration wins - toAppend.add(entry.tool()); - log.info( - "Auto-exposed workflow '{}' as agent_tool '{}' on '{}'", - entry.workflowName(), - entry.tool().getName(), - config.getName()); - } - if (!toAppend.isEmpty()) { - appendTools(config, toAppend); - } - } - - /** Typed view of an {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} entry. */ - private record AutoExposeSpec(String toolName, String description) {} - - /** - * Cached pairing of source workflow name + pre-built {@code agent_tool} - * {@link ToolConfig}. We carry the workflow name alongside the tool so - * the per-compile self-recursion guard (a workflow can't auto-expose - * itself as a tool on its own compile) stays correct without re-reading - * the {@link WorkflowDef} metadata. - */ - private record AutoExposedEntry(String workflowName, ToolConfig tool) {} - - /** - * Lazily build and cache the auto-exposed tool entries. Successful - * results are cached for the lifetime of the bean — registered - * server-side agents are written at server {@code @PostConstruct} and - * don't change at runtime. - * - *

    A transient DAO failure returns an empty list without - * caching, so the next compile retries the lookup. The merge is a - * convenience layer, not a correctness requirement — a failed lookup - * shouldn't fail the compile.

    - */ - private List autoExposedEntries() { - List snapshot = cachedAutoExposed; - if (snapshot != null) return snapshot; - if (metadataDAO == null) { - cachedAutoExposed = List.of(); - return cachedAutoExposed; - } - synchronized (this) { - if (cachedAutoExposed != null) return cachedAutoExposed; - try { - List defs = metadataDAO.getAllWorkflowDefsLatestVersions(); - List built = buildEntries(defs == null ? List.of() : defs); - cachedAutoExposed = built; - log.debug("auto-expose merge: fetched {} workflow def(s); cached {} auto-exposed entry(ies)", - defs == null ? 0 : defs.size(), built.size()); - return built; - } catch (Exception e) { - // NOT cached — let the next compile retry the DAO. - log.warn("auto-expose merge: metadataDAO lookup failed; will retry on next compile. {}", - e.getMessage()); - return List.of(); - } - } - } - - /** - * Project the DAO's full workflow-def list down to the auto-exposed - * subset, building the {@link ToolConfig} once per workflow. - */ - private static List buildEntries(List defs) { - List built = new ArrayList<>(); - for (WorkflowDef def : defs) { - AutoExposeSpec spec = readAutoExposeSpec(def); - if (spec == null) continue; - built.add(new AutoExposedEntry(def.getName(), buildAgentTool(def.getName(), spec))); - } - return built; - } - - /** Names of every tool already declared on {@code config}. */ - private static Set collectToolNames(AgentConfig config) { - if (config.getTools() == null) return new HashSet<>(); - Set names = new HashSet<>(); - for (ToolConfig t : config.getTools()) { - if (t.getName() != null) names.add(t.getName()); - } - return names; - } - - /** - * Read the auto-expose marker off a {@link WorkflowDef}, returning null - * when the marker is absent or malformed. Type-checks the spec map and - * its {@code name} field; defaults missing description to empty string. - */ - private static AutoExposeSpec readAutoExposeSpec(WorkflowDef def) { - Map metadata = def.getMetadata(); - if (metadata == null || !(metadata.get(AUTO_EXPOSE_AS_TOOL_METADATA_KEY) instanceof Map spec)) { - return null; - } - if (!(spec.get("name") instanceof String toolName) || toolName.isEmpty()) return null; - String description = spec.get("description") instanceof String s ? s : ""; - return new AutoExposeSpec(toolName, description); - } - - /** Build the agent_tool ToolConfig the LLM tool list ends up seeing. */ - private static ToolConfig buildAgentTool(String workflowName, AutoExposeSpec spec) { - return ToolConfig.builder() - .name(spec.toolName()) - .toolType("agent_tool") - .description(spec.description()) - .config(Map.of("workflowName", workflowName)) - .build(); - } - - /** Copy-on-write the tools list with the appended entries set back on {@code config}. */ - private static void appendTools(AgentConfig config, List toAppend) { - List merged = new ArrayList<>(config.getTools() != null ? config.getTools() : List.of()); - merged.addAll(toAppend); - config.setTools(merged); - } - // Setters for configuration public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; diff --git a/server/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java b/server/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java new file mode 100644 index 000000000..928531e08 --- /dev/null +++ b/server/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.compiler; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.netflix.conductor.common.metadata.workflow.WorkflowDef; +import com.netflix.conductor.dao.MetadataDAO; + +import dev.agentspan.runtime.model.AgentConfig; +import dev.agentspan.runtime.model.ToolConfig; + +/** + * Owns the "auto-expose registered sub-agents as tools" contract for + * {@link AgentCompiler}. + * + *

    The contract: any workflow registered in Conductor's metadata store + * carrying the {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} marker is silently + * appended to every top-level agent's tool list as an {@code agent_tool} + * at compile time. The OCG sub-agent (and any future server-registered + * sub-agent) becomes LLM-visible via this single mechanism — no per-feature + * injection class required.

    + * + *

    Lifecycle: the result of the DAO scan is cached on first successful + * read and never refreshed. Registered server-side agents are written at + * server {@code @PostConstruct} time and don't change at runtime, so + * re-querying the DAO per compile would be wasted work. A transient DAO + * failure is not cached — the next compile retries the lookup so + * a one-shot blip doesn't permanently hide registered agents.

    + * + *

    Bootstrap ordering: the registrar that writes auto-exposed agents + * must call {@link AgentCompiler#compileWithoutAutoExpose} during its + * {@code @PostConstruct} loop, never {@link AgentCompiler#compile}. + * Triggering the merge before the registrar finishes its + * {@code dao.updateWorkflowDef} writes would snapshot an empty list and + * freeze it for the bean's lifetime.

    + */ +@Component +public class AutoExposedToolsMerger { + + /** + * Metadata key on a {@link WorkflowDef} that marks the workflow as one + * that should be silently appended to every top-level agent's tool list + * at compile time. The value is a {@code Map} with at + * least {@code name} and {@code description}; both are surfaced to the + * agent's LLM via the {@code agent_tool} routing. + */ + public static final String AUTO_EXPOSE_AS_TOOL_METADATA_KEY = "agentspan.autoExposeAsTool"; + + private static final Logger log = LoggerFactory.getLogger(AutoExposedToolsMerger.class); + + /** + * Optional — null for unit tests that construct {@link AgentCompiler} + * directly. When null, {@link #merge(AgentConfig)} is a no-op. + */ + private final MetadataDAO metadataDAO; + + /** + * Lazy cache of the auto-exposed tool entries the DAO returns. Populated + * on first successful {@link #autoExposedEntries()} call and never + * refreshed. {@code volatile} for the double-checked-locking idiom. + */ + private volatile List cachedAutoExposed; + + @Autowired + public AutoExposedToolsMerger(@Autowired(required = false) MetadataDAO metadataDAO) { + this.metadataDAO = metadataDAO; + } + + /** A no-DAO merger that is always a no-op. For tests / direct construction. */ + public static AutoExposedToolsMerger disabled() { + return new AutoExposedToolsMerger(null); + } + + /** + * Append every DAO-registered workflow that carries the + * {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} marker to {@code config.tools} + * as an {@code agent_tool}. + * + *

    Mutates {@code config} in place. Skips when:

    + *
      + *
    • {@link #metadataDAO} is absent (unit-test path)
    • + *
    • The workflow being compiled IS the auto-exposed one + * — no self-recursion
    • + *
    • A tool with that name is already declared on the config + * — caller's explicit declaration wins
    • + *
    + */ + public void merge(AgentConfig config) { + if (config == null) return; + List entries = autoExposedEntries(); + if (entries.isEmpty()) return; + + Set takenNames = collectToolNames(config); + List toAppend = new ArrayList<>(); + for (AutoExposedEntry entry : entries) { + if (entry.workflowName().equals(config.getName())) continue; // self-recursion guard + if (!takenNames.add(entry.tool().getName())) continue; // caller's declaration wins + toAppend.add(entry.tool()); + log.info( + "Auto-exposed workflow '{}' as agent_tool '{}' on '{}'", + entry.workflowName(), + entry.tool().getName(), + config.getName()); + } + if (!toAppend.isEmpty()) { + appendTools(config, toAppend); + } + } + + /** Typed view of an {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} entry. */ + private record AutoExposeSpec(String toolName, String description) {} + + /** + * Cached pairing of source workflow name + pre-built {@code agent_tool} + * {@link ToolConfig}. The workflow name rides alongside the tool so the + * per-compile self-recursion guard stays correct without re-reading + * {@link WorkflowDef} metadata. + */ + private record AutoExposedEntry(String workflowName, ToolConfig tool) {} + + private List autoExposedEntries() { + List snapshot = cachedAutoExposed; + if (snapshot != null) return snapshot; + if (metadataDAO == null) { + cachedAutoExposed = List.of(); + return cachedAutoExposed; + } + synchronized (this) { + if (cachedAutoExposed != null) return cachedAutoExposed; + try { + List defs = metadataDAO.getAllWorkflowDefsLatestVersions(); + List built = buildEntries(defs == null ? List.of() : defs); + cachedAutoExposed = built; + log.debug( + "auto-expose merge: fetched {} workflow def(s); cached {} auto-exposed entry(ies)", + defs == null ? 0 : defs.size(), + built.size()); + return built; + } catch (Exception e) { + // NOT cached — let the next compile retry the DAO. + log.warn( + "auto-expose merge: metadataDAO lookup failed; will retry on next compile. {}", + e.getMessage()); + return List.of(); + } + } + } + + private static List buildEntries(List defs) { + List built = new ArrayList<>(); + for (WorkflowDef def : defs) { + AutoExposeSpec spec = readAutoExposeSpec(def); + if (spec == null) continue; + built.add(new AutoExposedEntry(def.getName(), buildAgentTool(def.getName(), spec))); + } + return built; + } + + private static Set collectToolNames(AgentConfig config) { + if (config.getTools() == null) return new HashSet<>(); + Set names = new HashSet<>(); + for (ToolConfig t : config.getTools()) { + if (t.getName() != null) names.add(t.getName()); + } + return names; + } + + private static AutoExposeSpec readAutoExposeSpec(WorkflowDef def) { + Map metadata = def.getMetadata(); + if (metadata == null || !(metadata.get(AUTO_EXPOSE_AS_TOOL_METADATA_KEY) instanceof Map spec)) { + return null; + } + if (!(spec.get("name") instanceof String toolName) || toolName.isEmpty()) return null; + String description = spec.get("description") instanceof String s ? s : ""; + return new AutoExposeSpec(toolName, description); + } + + private static ToolConfig buildAgentTool(String workflowName, AutoExposeSpec spec) { + return ToolConfig.builder() + .name(spec.toolName()) + .toolType("agent_tool") + .description(spec.description()) + .config(Map.of("workflowName", workflowName)) + .build(); + } + + private static void appendTools(AgentConfig config, List toAppend) { + List merged = new ArrayList<>(config.getTools() != null ? config.getTools() : List.of()); + merged.addAll(toAppend); + config.setTools(merged); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java index 349f9f543..29d3e8ace 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java @@ -39,7 +39,8 @@ public HttpRequest build(OcgProperties properties, Map input) { } private static void addQueryParamIfPresent(UriComponentsBuilder uri, String name, Object value) { - String s = StringUtils.defaultString(value == null ? null : value.toString()); + if (value == null) return; + String s = value.toString(); if (StringUtils.isNotEmpty(s)) { uri.queryParam(name, s); } diff --git a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java index 9f59d8cbe..29c34cc23 100644 --- a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java +++ b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java @@ -66,7 +66,15 @@ public void registerAll() { private void register(RegisteredAgent agent) { AgentConfig config = agent.agentConfig(); - WorkflowDef def = agentCompiler.compile(config); + // ``compileWithoutAutoExpose`` (not ``compile``) for two reasons: + // 1. Registered agents shouldn't have other registered agents + // auto-injected into them as tools. + // 2. The merger's lazy cache must not be triggered here — the + // registered defs aren't in the DAO yet at this point, so the + // first read would snapshot an empty list and freeze it for the + // bean's lifetime, silently hiding every registered agent from + // subsequent user compiles. + WorkflowDef def = agentCompiler.compileWithoutAutoExpose(config); ExposeAsTool expose = agent.autoExpose(); if (expose != null) { stampAutoExpose(def, expose); diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 653496895..4a5b52461 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -185,7 +185,8 @@ agentspan.credentials.resolve.rate-limit=120 agentspan.ocg.url=${OCG_URL:} agentspan.ocg.api-key=${OCG_API_KEY:} agentspan.ocg.model=${OCG_MODEL:openai/gpt-4o-mini} -agentspan.ocg.response-cap-chars=8192 +# Per-call response cap defaults to 8192 in OcgProperties. Uncomment to override. +# agentspan.ocg.response-cap-chars=8192 # Metrics conductor.metrics-prometheus.enabled=true diff --git a/server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java b/server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java new file mode 100644 index 000000000..5cb3f947b --- /dev/null +++ b/server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.compiler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.netflix.conductor.common.metadata.workflow.WorkflowDef; +import com.netflix.conductor.dao.MetadataDAO; + +import dev.agentspan.runtime.model.AgentConfig; +import dev.agentspan.runtime.model.ToolConfig; +import dev.agentspan.runtime.registry.RegisteredAgent; +import dev.agentspan.runtime.registry.RegisteredAgent.ExposeAsTool; +import dev.agentspan.runtime.registry.RegisteredAgentRegistrar; + +/** + * Bootstrap-ordering regression test for the {@link RegisteredAgentRegistrar} + * + auto-expose merger interaction. + * + *

    The merger lazily caches the DAO's auto-exposed-workflow list on first + * read and never refreshes it. If the registrar performs the cache-triggering + * read during its own bootstrap loop — i.e. before the agent it's + * registering has been written to the DAO — the cache snapshots an empty list + * and stays empty for the bean's lifetime. The OCG sub-agent (and every future + * server-registered agent) silently becomes invisible to user compiles.

    + * + *

    This test pins the contract by exercising the real registrar against a + * stateful in-memory DAO and verifying that, after bootstrap, a user-side + * merge picks up the agent the registrar just persisted.

    + */ +class RegisteredAgentBootstrapTest { + + @Test + void userMergeAfterBootstrapSeesAgentsTheRegistrarJustWrote() { + // Stateful in-memory DAO — getAllWorkflowDefsLatestVersions reflects + // whatever has been written so far. Mirrors Conductor's real + // persistence behaviour during boot. + MetadataDAO dao = mock(MetadataDAO.class); + List daoState = new ArrayList<>(); + when(dao.getAllWorkflowDefsLatestVersions()).thenAnswer(inv -> List.copyOf(daoState)); + doAnswer(inv -> { + daoState.add(inv.getArgument(0)); + return null; + }) + .when(dao) + .updateWorkflowDef(any(WorkflowDef.class)); + + AgentCompiler compiler = new AgentCompiler(dao); + + // Stub a registered agent whose compile path doesn't need a real + // model — the AgentConfig is built with no tools and no model so it + // routes through compileSimple (which only needs the name + builds a + // workflow). What matters here is the order: register → write → merge. + RegisteredAgent helper = new RegisteredAgent() { + @Override + public AgentConfig agentConfig() { + return AgentConfig.builder() + .name("_helper_agent") + .description("Test helper") + .model("openai/gpt-4o-mini") + .tools(new ArrayList<>()) + .build(); + } + + @Override + public ExposeAsTool autoExpose() { + return new ExposeAsTool("helper_tool", "Call when stuck."); + } + }; + + // Bootstrap path — this is where the cache-population bug fires + // pre-fix: the registrar's compile() call queries the (still-empty) + // DAO and caches an empty entry list. + new RegisteredAgentRegistrar(compiler, dao, List.of(helper)).registerAll(); + + // Sanity: the registrar wrote the auto-expose-marked def to the DAO. + assertThat(daoState).hasSize(1); + assertThat(daoState.get(0).getMetadata()) + .as("registrar must stamp the auto-expose metadata key") + .containsKey(AgentCompiler.AUTO_EXPOSE_AS_TOOL_METADATA_KEY); + + // The actual contract under test: a user agent compiled AFTER bootstrap + // must see the registered agent in its tools list. Pre-fix this fails + // because the cache was populated empty during step 1, before the DAO + // actually had the registered def in it. + AgentConfig userAgent = AgentConfig.builder() + .name("user_agent") + .tools(new ArrayList<>()) + .build(); + + compiler.mergeAutoExposedTools(userAgent); + + assertThat(userAgent.getTools()) + .extracting(ToolConfig::getName) + .as("first user merge after bootstrap must see the just-registered agent") + .contains("helper_tool"); + } +} diff --git a/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java b/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java index 9861ce27a..3ec3315f8 100644 --- a/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java +++ b/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java @@ -39,7 +39,7 @@ class RegisteredAgentRegistrarTest { void compilesAndRegistersEveryRegisteredAgent() { AgentCompiler compiler = mock(AgentCompiler.class); MetadataDAO dao = mock(MetadataDAO.class); - when(compiler.compile(any())).thenAnswer(inv -> { + when(compiler.compileWithoutAutoExpose(any())).thenAnswer(inv -> { AgentConfig cfg = inv.getArgument(0); WorkflowDef def = new WorkflowDef(); def.setName(cfg.getName()); @@ -51,8 +51,8 @@ void compilesAndRegistersEveryRegisteredAgent() { new RegisteredAgentRegistrar(compiler, dao, List.of(a, b)).registerAll(); - verify(compiler).compile(a.agentConfig()); - verify(compiler).compile(b.agentConfig()); + verify(compiler).compileWithoutAutoExpose(a.agentConfig()); + verify(compiler).compileWithoutAutoExpose(b.agentConfig()); ArgumentCaptor captor = ArgumentCaptor.forClass(WorkflowDef.class); verify(dao, org.mockito.Mockito.times(2)).updateWorkflowDef(captor.capture()); assertThat(captor.getAllValues().stream().map(WorkflowDef::getName)) @@ -66,7 +66,7 @@ void stampsAutoExposeMetadataWhenAgentRequestsIt() { // silently hide every server-side sub-agent from end-user agents. AgentCompiler compiler = mock(AgentCompiler.class); MetadataDAO dao = mock(MetadataDAO.class); - when(compiler.compile(any())).thenReturn(emptyDef("helper")); + when(compiler.compileWithoutAutoExpose(any())).thenReturn(emptyDef("helper")); RegisteredAgent agent = stubAgent("helper", new ExposeAsTool("helper_tool", "Call when stuck.")); @@ -88,7 +88,7 @@ void doesNotStampWhenAutoExposeReturnsNull() { // from the DAO without the auto-expose flag. AgentCompiler compiler = mock(AgentCompiler.class); MetadataDAO dao = mock(MetadataDAO.class); - when(compiler.compile(any())).thenReturn(emptyDef("internal")); + when(compiler.compileWithoutAutoExpose(any())).thenReturn(emptyDef("internal")); new RegisteredAgentRegistrar(compiler, dao, List.of(stubAgent("internal", null))).registerAll(); From 0b88cc40fcf821dcb21506ac9b2c3a5784d23d79 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Wed, 10 Jun 2026 11:38:50 -0700 Subject: [PATCH 15/61] style: apply spotless / palantir-java-format violations from CI CI flagged a handful of line-wrap differences in files touched by the previous fix. Local palantir-java-format 2.50.0 + Zulu JDK 21 throws NoSuchMethodError so spotlessApply is unavailable here; applying the hunks reported in the CI build log by hand. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../runtime/compiler/AgentCompiler.java | 3 +- .../compiler/AutoExposedToolsMerger.java | 3 +- .../compiler/AutoExposedToolsMergeTest.java | 31 +++++++------------ .../runtime/ocg/OcgRequestTaskTest.java | 3 +- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java b/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java index 9e7be1dc7..ea03a1893 100644 --- a/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java +++ b/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java @@ -48,7 +48,8 @@ public class AgentCompiler { * callers (registrar, tests) that previously imported it from this class * keep compiling without churn. */ - public static final String AUTO_EXPOSE_AS_TOOL_METADATA_KEY = AutoExposedToolsMerger.AUTO_EXPOSE_AS_TOOL_METADATA_KEY; + public static final String AUTO_EXPOSE_AS_TOOL_METADATA_KEY = + AutoExposedToolsMerger.AUTO_EXPOSE_AS_TOOL_METADATA_KEY; private int timeoutSeconds = 0; private int llmRetryCount = 3; diff --git a/server/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java b/server/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java index 928531e08..6a168a582 100644 --- a/server/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java +++ b/server/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java @@ -152,8 +152,7 @@ private List autoExposedEntries() { } catch (Exception e) { // NOT cached — let the next compile retry the DAO. log.warn( - "auto-expose merge: metadataDAO lookup failed; will retry on next compile. {}", - e.getMessage()); + "auto-expose merge: metadataDAO lookup failed; will retry on next compile. {}", e.getMessage()); return List.of(); } } diff --git a/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java b/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java index afa94c255..d4b4d269e 100644 --- a/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java +++ b/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java @@ -187,11 +187,10 @@ void mergeRunsOnceAtTopLevelOnlyAndSkipsInternalRecursion() { // compileSubAgent is the entry that nested compilation goes through. // It must NOT mutate ``inner.tools`` with the auto-exposed entry. - compiler.compileSubAgent( - inner, "inner_ref", "${workflow.input.prompt}", "${workflow.input.media}", null); + compiler.compileSubAgent(inner, "inner_ref", "${workflow.input.prompt}", "${workflow.input.media}", null); - boolean innerHasAutoExposed = inner.getTools() != null - && inner.getTools().stream().anyMatch(t -> "helper_agent".equals(t.getName())); + boolean innerHasAutoExposed = + inner.getTools() != null && inner.getTools().stream().anyMatch(t -> "helper_agent".equals(t.getName())); assertThat(innerHasAutoExposed) .as("nested sub-agent must NOT have the auto-exposed tool merged into its tool list") .isFalse(); @@ -206,14 +205,10 @@ void daoQueriedOnlyOnceAcrossMultipleMerges() { WorkflowDef flagged = wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "x"); when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(flagged)); - AgentConfig a = AgentConfig.builder() - .name("agent_a") - .tools(new ArrayList<>()) - .build(); - AgentConfig b = AgentConfig.builder() - .name("agent_b") - .tools(new ArrayList<>()) - .build(); + AgentConfig a = + AgentConfig.builder().name("agent_a").tools(new ArrayList<>()).build(); + AgentConfig b = + AgentConfig.builder().name("agent_b").tools(new ArrayList<>()).build(); compiler.mergeAutoExposedTools(a); compiler.mergeAutoExposedTools(b); @@ -233,14 +228,10 @@ void daoFailureIsNotCachedAndIsRetriedOnNextMerge() { .thenThrow(new RuntimeException("transient DAO failure")) .thenReturn(List.of(wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "x"))); - AgentConfig first = AgentConfig.builder() - .name("first") - .tools(new ArrayList<>()) - .build(); - AgentConfig second = AgentConfig.builder() - .name("second") - .tools(new ArrayList<>()) - .build(); + AgentConfig first = + AgentConfig.builder().name("first").tools(new ArrayList<>()).build(); + AgentConfig second = + AgentConfig.builder().name("second").tools(new ArrayList<>()).build(); compiler.mergeAutoExposedTools(first); compiler.mergeAutoExposedTools(second); diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java index eeffe0deb..1e6bd2665 100644 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java @@ -282,8 +282,7 @@ void memorySetOperationPostsToMemoriesEndpointAndStripsAgentspanCtx() throws Exc void memoryReinforceOperationPostsToReinforceEndpointWithFilteredBody() throws Exception { HttpClient http = mock(HttpClient.class); stubSend(http, stub(200, "{\"reinforced\":true}")); - OcgRequestTask task = - new OcgRequestTask(new OcgMemoryReinforceOperation(), props("http://ocg.local"), http); + OcgRequestTask task = new OcgRequestTask(new OcgMemoryReinforceOperation(), props("http://ocg.local"), http); Map input = new LinkedHashMap<>(); input.put("key", "k1"); From 1e590a237567b24f2bb80d4375247016dd0c667f Mon Sep 17 00:00:00 2001 From: nicholascole Date: Wed, 10 Jun 2026 11:41:18 -0700 Subject: [PATCH 16/61] style: collapse remaining spotless violation in OcgRequestTaskTest Last hunk CI flagged after the prior fixup landed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java index 1e6bd2665..1cba269fb 100644 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java @@ -302,8 +302,7 @@ void memoryReinforceOperationPostsToReinforceEndpointWithFilteredBody() throws E ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); verify(http).send(req.capture(), any()); // key is part of the URL; the body must be the picked subset. - assertThat(req.getValue().uri().toString()) - .isEqualTo("http://ocg.local/api/v1/memories/k1/reinforce"); + assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/api/v1/memories/k1/reinforce"); assertThat(req.getValue().method()).isEqualTo("POST"); @SuppressWarnings("unchecked") From 6d69f1807199f7dcc96511bb6a15737dda0fd222 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Wed, 10 Jun 2026 13:00:50 -0700 Subject: [PATCH 17/61] feat(ocg): require OCG_MODEL explicitly when OCG is enabled Drop the silent ``openai/gpt-4o-mini`` default for ``agentspan.ocg.model``. The right model depends on cost, latency, and the OCG corpus, so it has to be an explicit operator decision rather than an inherited fallback. ``OcgAgentFactory.build`` now throws ``IllegalArgumentException`` with an operator-actionable message when ``OCG_URL`` is set but ``OCG_MODEL`` is blank, so boot fails fast instead of silently routing OCG traffic through the wrong model. ``OCG_MODEL`` is documented as required (alongside ``OCG_URL``) in the docs setup table; the optional-knobs table no longer lists it. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/ocg-agent-flow.md | 16 +++++++++------- .../agentspan/runtime/ocg/OcgAgentFactory.java | 8 ++++++++ .../dev/agentspan/runtime/ocg/OcgProperties.java | 9 +++++++-- server/src/main/resources/application.properties | 3 ++- .../runtime/ocg/OcgAgentFactoryTest.java | 11 +++++++++++ 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/docs/ocg-agent-flow.md b/docs/ocg-agent-flow.md index 9e2a6402e..362b8eb56 100644 --- a/docs/ocg-agent-flow.md +++ b/docs/ocg-agent-flow.md @@ -14,12 +14,13 @@ plugs in as one `@Component` without touching `AgentCompiler`, ## Setup — integrating OCG with AgentSpan -OCG is fully opt-in. The integration is **two environment variables** the +OCG is fully opt-in. The integration is **three environment variables** the AgentSpan server reads at startup: | Env var | Required? | What it does | | ------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- | | `OCG_URL` | **Yes** (to enable) | Base URL of your OCG instance, e.g. `https://dev.orkescontextgraph.io`. If unset or empty, every OCG bean stays out of the Spring context, no `_ocg_agent` workflow is registered, and no user agent gets the auto-injected `ocg_agent` tool. The feature is completely dormant. | +| `OCG_MODEL` | **Yes** (when OCG is enabled) | LLM the OCG sub-agent uses for its own turns, e.g. `openai/gpt-4o-mini`, `anthropic/claude-haiku-4-5`. No silent default — boot fails fast with a clear error if `OCG_URL` is set but `OCG_MODEL` is blank. The right model depends on cost/latency targets and the OCG corpus you're querying, so it has to be an explicit operator decision. | | `OCG_API_KEY` | Yes (if OCG requires auth) | Bearer token sent as `Authorization: Bearer ` on every OCG HTTP request. Empty means no auth header — fine for unauthenticated local OCG instances; required for the hosted dev / prod instances. | ### Local dev @@ -30,11 +31,12 @@ or `java -jar`): ```bash export OCG_URL=https://dev.orkescontextgraph.io export OCG_API_KEY= -export OPENAI_API_KEY=sk-... # the OCG sub-agent also needs an LLM key +export OCG_MODEL=openai/gpt-4o-mini # required when OCG is enabled +export OPENAI_API_KEY=sk-... # provider key for whatever OCG_MODEL points at ./gradlew bootRun ``` -In IntelliJ, add the same three to your Spring Boot run configuration's +In IntelliJ, add the same four to your Spring Boot run configuration's **Environment variables** field. ### Docker / production @@ -46,6 +48,7 @@ to Spring properties via `application.properties`: ``` agentspan.ocg.url=${OCG_URL:} agentspan.ocg.api-key=${OCG_API_KEY:} +agentspan.ocg.model=${OCG_MODEL:} ``` So you can alternatively pass them as Spring properties on the JVM @@ -80,10 +83,9 @@ state. ### Optional tuning knobs -| Property | Default | Effect | -| ---------------------------------- | -------------------- | ---------------------------------------------------------------------------- | -| `agentspan.ocg.model` | `openai/gpt-4o-mini` | LLM the OCG sub-agent uses internally. Override via `OCG_MODEL` env or `-Dagentspan.ocg.model=…`. | -| `agentspan.ocg.response-cap-chars` | `8192` | Per-call response truncation budget. Raise if your model context allows; lower to save tokens. | +| Property | Default | Effect | +| ---------------------------------- | ------- | ---------------------------------------------------------------------------- | +| `agentspan.ocg.response-cap-chars` | `8192` | Per-call response truncation budget. Raise if your model context allows; lower to save tokens. | --- diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java index 1b1e39aee..c61e7728a 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java @@ -11,6 +11,8 @@ import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; + import dev.agentspan.runtime.model.AgentConfig; import dev.agentspan.runtime.model.ToolConfig; @@ -101,6 +103,12 @@ public final class OcgAgentFactory { private OcgAgentFactory() {} public static AgentConfig build(OcgProperties props) { + if (StringUtils.isBlank(props.getModel())) { + throw new IllegalArgumentException( + "OCG is enabled (agentspan.ocg.url is set) but agentspan.ocg.model is blank. " + + "Set OCG_MODEL (or -Dagentspan.ocg.model=…) to the LLM the OCG sub-agent " + + "should use, e.g. OCG_MODEL=openai/gpt-4o-mini."); + } String prompt = OCG_SYSTEM_PROMPT.replace( TODAY_PLACEHOLDER, LocalDate.now(ZoneOffset.UTC).toString()); return AgentConfig.builder() diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java index 2e5d03be0..b2c7829a8 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java @@ -33,8 +33,13 @@ public class OcgProperties { */ private String apiKey; - /** Model used by the OCG sub-agent's LLM turns. */ - private String model = "openai/gpt-4o-mini"; + /** + * Model the OCG sub-agent uses for its own LLM turns. Required when OCG + * is enabled — no silent default, because the right model here depends + * on cost, latency, and the OCG corpus the operator is querying. Boot + * fails fast in {@link OcgAgentFactory#build} when this is blank. + */ + private String model; /** * Per-response truncation cap (post-projection, JSON-serialized) for the diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 4a5b52461..c872284b2 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -184,7 +184,8 @@ agentspan.credentials.resolve.rate-limit=120 # so the main agent's LLM can delegate to OCG when it needs context. agentspan.ocg.url=${OCG_URL:} agentspan.ocg.api-key=${OCG_API_KEY:} -agentspan.ocg.model=${OCG_MODEL:openai/gpt-4o-mini} +# Required when OCG is enabled. Boot fails fast if OCG_URL is set but OCG_MODEL is not. +agentspan.ocg.model=${OCG_MODEL:} # Per-call response cap defaults to 8192 in OcgProperties. Uncomment to override. # agentspan.ocg.response-cap-chars=8192 diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java index 06568f3a5..87a3f7ba4 100644 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java @@ -6,6 +6,7 @@ package dev.agentspan.runtime.ocg; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.time.LocalDate; import java.time.ZoneOffset; @@ -84,6 +85,16 @@ void systemPromptHasTodayUtcDateSubstituted() { assertThat(prompt).doesNotContain("{{TODAY}}"); } + @Test + void buildFailsFastWhenModelIsBlank() { + OcgProperties noModel = new OcgProperties(); + noModel.setUrl("http://ocg.local"); + // model deliberately unset — operator forgot OCG_MODEL. + assertThatThrownBy(() -> OcgAgentFactory.build(noModel)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("OCG_MODEL"); + } + @Test void queryToolDeclaresQueryAsRequiredInput() { ToolConfig queryTool = OcgAgentFactory.build(props()).getTools().stream() From cd4cb8f733ff40076ff3a1113c3dbce5cebbcaab Mon Sep 17 00:00:00 2001 From: Viren Baraiya Date: Wed, 10 Jun 2026 13:38:44 -0700 Subject: [PATCH 18/61] agentdef --- sdk/java/docs/api-reference.md | 44 ++ sdk/java/docs/concepts/agents.md | 70 +++ .../ai/examples/Example70AnnotatedAgent.java | 47 ++ .../org/conductoross/conductor/ai/Agent.java | 51 +- .../conductor/ai/annotations/AgentDef.java | 133 +++++ .../ai/internal/AgentConfigSerializer.java | 16 +- .../conductor/ai/internal/AgentRegistry.java | 308 ++++++++++ .../conductor/ai/AgentAnnotationTest.java | 532 ++++++++++++++++++ 8 files changed, 1194 insertions(+), 7 deletions(-) create mode 100644 sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example70AnnotatedAgent.java create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/AgentDef.java create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentRegistry.java create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/AgentAnnotationTest.java diff --git a/sdk/java/docs/api-reference.md b/sdk/java/docs/api-reference.md index 691a1dd24..2b1b00bb4 100644 --- a/sdk/java/docs/api-reference.md +++ b/sdk/java/docs/api-reference.md @@ -79,6 +79,7 @@ Agent.builder() .name(String) // required .model(String) // required .instructions(String) + .instructions(Supplier) // dynamic — re-evaluated on each run submission .instructionsTemplate(PromptTemplate) .introduction(String) .metadata(Map) @@ -162,6 +163,49 @@ Agent.builder() --- +## @AgentDef annotation + +Declarative alternative to the builder — annotate a method to define an agent +(see [Agents](concepts/agents.md#agentdef-annotation) for attribute details): + +```java +import org.conductoross.conductor.ai.annotations.AgentDef; + +public class Weather { + @Tool(name = "get_weather", description = "Get weather for a city") + public String getWeather(String city) { return "Sunny, 72F in " + city; } + + @AgentDef(model = "openai/gpt-4o") // @Tool methods attach automatically + public String weatherbot() { + // returned String = instructions; no-arg form is lazy — re-evaluated per run + return "You are a weather assistant. Today is " + LocalDate.now() + "."; + } + + @AgentDef(model = "openai/gpt-4o") // optional Agent.Builder param = full builder API + public void researcher(Agent.Builder builder) { + builder.termination(new MaxMessageTermination(10)); + } + + @AgentDef // return Agent (or Agent.Builder) = full factory + public Agent reviewer() { + return Agent.builder().name("reviewer").model("openai/gpt-4o") + .instructions("Review the draft.").build(); + } + + @AgentDef(model = "openai/gpt-4o") // return PromptTemplate = server-side template + public PromptTemplate support() { + return new PromptTemplate("customer-support", Map.of("tone", "friendly")); + } +} +``` + +```java +List agents = Agent.fromInstance(instance); // resolve all @AgentDef methods +Agent agent = Agent.fromInstance(instance, "name"); // resolve one by name +``` + +--- + ## AgentResult ```java diff --git a/sdk/java/docs/concepts/agents.md b/sdk/java/docs/concepts/agents.md index 11d058c5c..8942e3535 100644 --- a/sdk/java/docs/concepts/agents.md +++ b/sdk/java/docs/concepts/agents.md @@ -14,6 +14,76 @@ Agent agent = Agent.builder() Every field below is optional. +### @AgentDef annotation + +Instead of the builder, a method can be annotated with `@AgentDef` (the Java counterpart of the Python SDK's `@agent` decorator). The method body returns the instructions; `@Tool` and `@GuardrailDef` methods on the same object are attached automatically. + +```java +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.annotations.AgentDef; +import org.conductoross.conductor.ai.annotations.Tool; + +public class Weather { + @Tool(name = "get_weather", description = "Get weather for a city") + public String getWeather(String city) { return "Sunny, 72F in " + city; } + + @AgentDef(model = "openai/gpt-4o") + public String weatherbot() { + return "You are a weather assistant."; + } +} + +Agent agent = Agent.fromInstance(new Weather(), "weatherbot"); +``` + +| Attribute | Default | Description | +|---|---|---| +| `name` | method name | Agent name. | +| `model` | `""` | `"provider/model"`. When empty and used as a sub-agent, inherits the parent's model. | +| `instructions` | `""` | Static system prompt. A non-empty `String` returned by the method wins over this attribute. | +| `tools` | `{"*"}` | Names of `@Tool` methods on the same object. `{"*"}` = all, `{}` = none. | +| `guardrails` | `{"*"}` | Names of `@GuardrailDef` methods on the same object. Same wildcard rules. | +| `agents` | `{}` | Names of other `@AgentDef` methods on the same object, used as sub-agents. | +| `strategy` | `HANDOFF` | Multi-agent strategy. | +| `maxTurns` | `25` | Maximum agent loop iterations. | +| `maxTokens` | unset | LLM `max_tokens` (`0` = unset). | +| `temperature` | unset | Sampling temperature (`NaN` = unset). | +| `credentials` | `{}` | Agent-level credential names. | +| `contextWindowBudget` | unset | Proactive condensation threshold (`0` = unset). | + +**Method contract.** The return type declares what the method provides: + +| Return type | Meaning | +|---|---| +| `void` | Nothing — the annotation attributes alone define the agent. | +| `String` | Dynamic instructions. A no-arg method is **lazy**: re-invoked on every run submission (when the config is serialized), so the prompt can reflect current state. A non-empty result wins over the `instructions` attribute. | +| `PromptTemplate` | Server-side instructions template (`instructionsTemplate`); invoked once. | +| `Agent.Builder` | The definition itself — the returned builder is built. | +| `Agent` | The definition itself, returned as-is (full factory, CrewAI-style). | + +The method may take no parameters, or a single `Agent.Builder` parameter — the escape hatch to the full builder API. The builder arrives pre-populated from the annotation and the discovered tools/guardrails/sub-agents; the method body can then apply anything the builder supports, including sub-agents defined in other classes. Builder-param methods are invoked exactly once (a customizer must not be replayed per run): + +```java +public class Research { + @AgentDef(model = "openai/gpt-4o", instructions = "You are a researcher.") + public void researcher(Agent.Builder builder) { + builder.termination(new MaxMessageTermination(10)) + .agents(Agent.fromInstance(new Editing(), "editor")); + } + + // full factory — annotation is a discovery marker; attributes other than name are rejected + @AgentDef + public Agent reviewer() { + return Agent.builder().name("reviewer").model("openai/gpt-4o") + .instructions("Review the draft.").build(); + } +} +``` + +`Agent.fromInstance(obj)` resolves all `@AgentDef` methods on an object; `Agent.fromInstance(obj, "name")` resolves one. For factory methods the lookup name is still the annotation `name`/method name, while the agent keeps the name the factory set. + +Dynamic instructions are also available directly on the builder, without the annotation: `Agent.builder().instructions(() -> "Today is " + LocalDate.now())` — the supplier is re-evaluated on each run submission, matching the Python SDK's callable instructions. + ### Identity | Builder method | Type | Default | Description | diff --git a/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example70AnnotatedAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example70AnnotatedAgent.java new file mode 100644 index 000000000..7896dc0b0 --- /dev/null +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example70AnnotatedAgent.java @@ -0,0 +1,47 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.examples; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.AgentDef; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.model.AgentResult; + +/** + * Example 70 — Annotated Agent + * + *

    Demonstrates defining an agent declaratively with the {@code @AgentDef} method + * annotation (the Java counterpart of the Python SDK's {@code @agent} decorator). + * The method body returns the agent's instructions; {@code @Tool} methods on the + * same class are attached automatically. + * + *

    Requirements: + *

      + *
    • AGENTSPAN_SERVER_URL=http://localhost:6767/api
    • + *
    • AGENTSPAN_LLM_MODEL=openai/gpt-4o
    • + *
    + */ +public class Example70AnnotatedAgent { + + @Tool(name = "get_weather", description = "Get the current weather for a city") + public String getWeather(String city) { + return "Sunny, 72F in " + city; + } + + @AgentDef(model = "openai/gpt-4o") + public String weatherbot() { + return "You are a weather assistant. Use the get_weather tool to answer questions."; + } + + public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); + Agent agent = Agent.fromInstance(new Example70AnnotatedAgent(), "weatherbot"); + + AgentResult result = runtime.run(agent, "What's the weather in Paris?"); + result.printResult(); + + runtime.shutdown(); + } +} diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/Agent.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/Agent.java index 4c270d475..5a34dc31a 100644 --- a/sdk/java/src/main/java/org/conductoross/conductor/ai/Agent.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/Agent.java @@ -43,7 +43,11 @@ public class Agent { private final String name; private final String model; - private final String instructions; + /** System prompt, held as a supplier so dynamic instructions are re-evaluated + * each time the config is serialized (every run submission) — matching the + * Python SDK, where callable instructions resolve at serialization time. */ + private final java.util.function.Supplier instructions; + private final List tools; private final List agents; private final Strategy strategy; @@ -230,7 +234,7 @@ public String getModel() { } public String getInstructions() { - return instructions; + return instructions == null ? null : instructions.get(); } public List getTools() { @@ -429,6 +433,35 @@ public static Builder builder() { return new Builder(); } + /** + * Resolve all {@link org.conductoross.conductor.ai.annotations.AgentDef @AgentDef}-annotated + * methods on an object into Agent instances. + * + *

    {@link org.conductoross.conductor.ai.annotations.Tool @Tool} and + * {@link org.conductoross.conductor.ai.annotations.GuardrailDef @GuardrailDef} methods + * on the same object are attached to each agent (all by default; filter with the + * annotation's {@code tools}/{@code guardrails} attributes). + * + * @param instance the object whose annotated methods define agents + * @return the resolved agents + */ + public static List fromInstance(Object instance) { + return org.conductoross.conductor.ai.internal.AgentRegistry.fromInstance(instance); + } + + /** + * Resolve a single {@link org.conductoross.conductor.ai.annotations.AgentDef @AgentDef}-annotated + * method by agent name (the annotation {@code name}, or the method name if unset). + * + * @param instance the object whose annotated methods define agents + * @param name the agent name to resolve + * @return the resolved agent + * @throws IllegalArgumentException if no agent with that name is defined on the object + */ + public static Agent fromInstance(Object instance, String name) { + return org.conductoross.conductor.ai.internal.AgentRegistry.fromInstance(instance, name); + } + @Override public String toString() { if (isExternal()) { @@ -449,7 +482,7 @@ public String toString() { public static class Builder { private String name; private String model; - private String instructions; + private java.util.function.Supplier instructions; private List tools; private List agents; private Strategy strategy = Strategy.HANDOFF; @@ -513,6 +546,18 @@ public Builder model(String model) { /** Set the system prompt / instructions for the agent. */ public Builder instructions(String instructions) { + this.instructions = instructions == null ? null : () -> instructions; + return this; + } + + /** + * Set dynamic instructions. The supplier is re-evaluated every time the + * agent config is serialized — i.e. on each run submission — so the prompt + * can reflect current state (date, feature flags, fetched context). + * Matches the Python SDK, where callable instructions resolve at + * serialization time. + */ + public Builder instructions(java.util.function.Supplier instructions) { this.instructions = instructions; return this; } diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/AgentDef.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/AgentDef.java new file mode 100644 index 000000000..98945c668 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/AgentDef.java @@ -0,0 +1,133 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.conductoross.conductor.ai.enums.Strategy; + +/** + * Marks a method as an agent definition. + * + *

    The Java counterpart of the Python SDK's {@code @agent} decorator. Annotate a + * method to define an agent declaratively; resolve it into an + * {@link org.conductoross.conductor.ai.Agent} with + * {@link org.conductoross.conductor.ai.Agent#fromInstance(Object)} or + * {@link org.conductoross.conductor.ai.Agent#fromInstance(Object, String)}. + * + *

    The method's return type declares what it provides: + *

      + *
    • {@code void} — nothing; the annotation attributes alone define the agent.
    • + *
    • {@code String} — dynamic instructions. A no-arg method is lazy: it is + * re-invoked every time the agent config is serialized (each run submission), + * so the prompt can reflect current state — matching the Python SDK, where + * callable instructions resolve at serialization time. A non-empty result wins + * over the {@link #instructions()} attribute.
    • + *
    • {@code PromptTemplate} — a server-side instructions template + * (sets {@code instructionsTemplate}); invoked once.
    • + *
    • {@code Agent.Builder} — the definition itself; the returned builder is built.
    • + *
    • {@code Agent} — the definition itself, returned as-is (CrewAI-style full + * factory). For no-arg factory forms, annotation attributes other than + * {@link #name()} are rejected — they would be silently ignored.
    • + *
    + * + *

    The method may take no parameters, or a single + * {@link org.conductoross.conductor.ai.Agent.Builder Agent.Builder} parameter as an + * escape hatch: the builder arrives pre-populated from the annotation (and the + * discovered tools/guardrails/sub-agents), and the method body can apply anything + * the builder supports — termination conditions, handoffs, memory, sub-agents from + * other classes, etc. Builder-param methods are invoked exactly once (re-running a + * customizer per serialization would replay its side effects). + * + *

    {@link Tool} and {@link GuardrailDef} methods declared on the same object are + * attached to the agent automatically (all of them by default — see {@link #tools()} + * and {@link #guardrails()}). Sub-agents are referenced by name via {@link #agents()}. + * + *

    Example: + *

    {@code
    + * public class Weather {
    + *     @Tool(description = "Get weather for a city")
    + *     public String getWeather(String city) { return "Sunny, 72F in " + city; }
    + *
    + *     @AgentDef(model = "openai/gpt-4o")
    + *     public String weatherbot() {
    + *         return "You are a weather assistant. Today is " + LocalDate.now() + ".";
    + *     }
    + *
    + *     // builder customizer: full builder API available
    + *     @AgentDef(model = "openai/gpt-4o", instructions = "You are a researcher.")
    + *     public void researcher(Agent.Builder builder) {
    + *         builder.termination(new MaxMessageTermination(10))
    + *                .agents(Agent.fromInstance(new Editing(), "editor"));
    + *     }
    + *
    + *     // full factory: the method builds the whole definition
    + *     @AgentDef
    + *     public Agent reviewer() {
    + *         return Agent.builder().name("reviewer").model("openai/gpt-4o")
    + *                 .instructions("Review the draft.").build();
    + *     }
    + * }
    + *
    + * Agent agent = Agent.fromInstance(new Weather(), "weatherbot");
    + * }
    + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface AgentDef { + /** Agent name. Defaults to the method name if not specified. */ + String name() default ""; + + /** + * LLM model in "provider/model" format (e.g. "openai/gpt-4o"). + * When empty and this agent is referenced as a sub-agent, the parent's + * model is inherited at resolution time. + */ + String model() default ""; + + /** + * Static system prompt. A non-empty {@code String} returned by the annotated + * method takes precedence over this attribute. + */ + String instructions() default ""; + + /** + * Names of {@link Tool}-annotated methods on the same object to attach. + * The default {@code {"*"}} attaches all of them; an empty array attaches none. + */ + String[] tools() default {"*"}; + + /** + * Names of {@link GuardrailDef}-annotated methods on the same object to attach. + * The default {@code {"*"}} attaches all of them; an empty array attaches none. + */ + String[] guardrails() default {"*"}; + + /** + * Names of other {@code @AgentDef}-annotated methods on the same object to use + * as sub-agents for multi-agent orchestration. + */ + String[] agents() default {}; + + /** Multi-agent orchestration strategy. Only meaningful when {@link #agents()} is set. */ + Strategy strategy() default Strategy.HANDOFF; + + /** Maximum number of agent loop iterations. */ + int maxTurns() default 25; + + /** Maximum tokens for LLM generation. 0 means unset (server default applies). */ + int maxTokens() default 0; + + /** Sampling temperature. NaN means unset (server default applies). */ + double temperature() default Double.NaN; + + /** Agent-level credential names to inject into the execution context. */ + String[] credentials() default {}; + + /** Token budget for proactive context condensation. 0 means unset. */ + int contextWindowBudget() default 0; +} diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentConfigSerializer.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentConfigSerializer.java index 43275aac1..19b9e479f 100644 --- a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentConfigSerializer.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentConfigSerializer.java @@ -72,8 +72,11 @@ private Map serializeAgent(Agent agent) { map.put("model", agent.getModel()); } // OpenAI uses `instructions`; ADK uses `instruction` (singular). - if (agent.getInstructions() != null && !agent.getInstructions().isEmpty()) { - map.put("google_adk".equals(fw) ? "instruction" : "instructions", agent.getInstructions()); + // Resolve once: dynamic instructions are supplier-backed and must not + // be re-evaluated within a single serialization. + String fwInstructions = agent.getInstructions(); + if (fwInstructions != null && !fwInstructions.isEmpty()) { + map.put("google_adk".equals(fw) ? "instruction" : "instructions", fwInstructions); } // Tools: framework normalizers (OpenAINormalizer, GoogleADKNormalizer) // expect the worker_ref shape `{_worker_ref, description, parameters}` @@ -170,8 +173,13 @@ private Map serializeAgent(Agent agent) { tmpl.put("version", pt.getVersion()); } agentMap.put("instructions", tmpl); - } else if (agent.getInstructions() != null && !agent.getInstructions().isEmpty()) { - agentMap.put("instructions", agent.getInstructions()); + } else { + // Resolve once: dynamic instructions are supplier-backed and must not + // be re-evaluated within a single serialization. + String instructions = agent.getInstructions(); + if (instructions != null && !instructions.isEmpty()) { + agentMap.put("instructions", instructions); + } } // Tools diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentRegistry.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentRegistry.java new file mode 100644 index 000000000..cefc90e94 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentRegistry.java @@ -0,0 +1,308 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.annotations.AgentDef; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.PromptTemplate; +import org.conductoross.conductor.ai.model.ToolDef; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Discovers {@link AgentDef}-annotated methods + * via reflection and resolves them into {@link Agent} instances. + * + *

    Parallel to {@link ToolRegistry}. Prefer the public entry points + * {@link Agent#fromInstance(Object)} and {@link Agent#fromInstance(Object, String)}. + */ +public final class AgentRegistry { + private static final Logger logger = LoggerFactory.getLogger(AgentRegistry.class); + + private AgentRegistry() {} + + /** + * Resolve all {@code @AgentDef}-annotated methods on an object into Agent instances. + * + * @param obj the object to inspect + * @return list of resolved agents + */ + public static List fromInstance(Object obj) { + Map methods = agentMethods(obj); + List agents = new ArrayList<>(); + for (Map.Entry entry : methods.entrySet()) { + agents.add(resolve(obj, methods, entry.getKey(), "", new ArrayDeque<>())); + } + return agents; + } + + /** + * Resolve a single {@code @AgentDef}-annotated method by its resolved agent name. + * + * @param obj the object to inspect + * @param name the agent name (annotation {@code name} or the method name) + * @return the resolved agent + * @throws IllegalArgumentException if no agent with that name is defined on the object + */ + public static Agent fromInstance(Object obj, String name) { + Map methods = agentMethods(obj); + if (!methods.containsKey(name)) { + throw new IllegalArgumentException("No @AgentDef method named '" + name + "' on " + + obj.getClass().getName() + ". Available: " + methods.keySet()); + } + return resolve(obj, methods, name, "", new ArrayDeque<>()); + } + + /** Discover all {@code @AgentDef}-annotated methods, keyed by resolved agent name. */ + private static Map agentMethods(Object obj) { + Map methods = new LinkedHashMap<>(); + for (Method method : obj.getClass().getMethods()) { + AgentDef ann = method.getAnnotation(AgentDef.class); + if (ann == null) continue; + String name = ann.name().isEmpty() ? method.getName() : ann.name(); + Method previous = methods.put(name, method); + if (previous != null) { + throw new IllegalArgumentException("Duplicate @AgentDef name '" + name + "' on " + + obj.getClass().getName() + " (methods " + previous.getName() + " and " + + method.getName() + ")"); + } + } + return methods; + } + + private static Agent resolve( + Object obj, Map methods, String name, String parentModel, Deque stack) { + if (stack.contains(name)) { + List cycle = new ArrayList<>(stack); + cycle.add(name); + throw new IllegalArgumentException("Cyclic @AgentDef sub-agent reference: " + String.join(" -> ", cycle)); + } + stack.push(name); + try { + Method method = methods.get(name); + AgentDef ann = method.getAnnotation(AgentDef.class); + validateSignature(obj, method); + + // Pure factory: a no-arg method returning Agent or Agent.Builder builds the + // whole definition itself (CrewAI-style). The annotation is a discovery + // marker only — non-default attributes would be silently ignored, so reject. + Class returnType = method.getReturnType(); + boolean pureFactory = + method.getParameterCount() == 0 && (returnType == Agent.class || returnType == Agent.Builder.class); + if (pureFactory) { + requireDiscoveryOnlyAttributes(obj, method, ann); + return buildFromFactoryResult(obj, method, invoke(obj, method)); + } + + String model = !ann.model().isEmpty() ? ann.model() : parentModel; + + Agent.Builder builder = Agent.builder() + .name(name) + .instructions(ann.instructions()) + .maxTurns(ann.maxTurns()) + .strategy(ann.strategy()); + if (!model.isEmpty()) builder.model(model); + if (ann.maxTokens() > 0) builder.maxTokens(ann.maxTokens()); + if (!Double.isNaN(ann.temperature())) builder.temperature(ann.temperature()); + if (ann.credentials().length > 0) builder.credentials(Arrays.asList(ann.credentials())); + if (ann.contextWindowBudget() > 0) builder.contextWindowBudget(ann.contextWindowBudget()); + + List tools = + selectByName(ToolRegistry.fromInstance(obj), ToolDef::getName, ann.tools(), "@Tool", obj); + if (!tools.isEmpty()) builder.tools(tools); + + List guardrails = selectByName( + ToolRegistry.guardrailsFromInstance(obj), + GuardrailDef::getName, + ann.guardrails(), + "@GuardrailDef", + obj); + if (!guardrails.isEmpty()) builder.guardrails(guardrails); + + if (ann.agents().length > 0) { + List subAgents = new ArrayList<>(); + for (String subName : ann.agents()) { + if (!methods.containsKey(subName)) { + throw new IllegalArgumentException("Sub-agent '" + subName + "' referenced by @AgentDef '" + + name + "' not found on " + obj.getClass().getName() + + ". Available: " + methods.keySet()); + } + subAgents.add(resolve(obj, methods, subName, model, stack)); + } + builder.agents(subAgents); + } + + Agent agent = invokeAgentMethod(obj, method, ann, builder); + logger.debug("Resolved agent '{}' from {}", name, obj.getClass().getSimpleName()); + return agent; + } finally { + stack.pop(); + } + } + + /** + * Enforce the {@code @AgentDef} method contract. The return type declares what + * the method provides: + *

      + *
    • {@code void} — nothing; the annotation alone defines the agent
    • + *
    • {@code String} — dynamic instructions
    • + *
    • {@code PromptTemplate} — a server-side instructions template
    • + *
    • {@code Agent.Builder} — the definition itself; the returned builder is built
    • + *
    • {@code Agent} — the definition itself, returned as-is (full factory)
    • + *
    + * Parameters: none, or a single {@link Agent.Builder} (pre-populated from the + * annotation and discovered tools/guardrails/sub-agents). + */ + private static void validateSignature(Object obj, Method method) { + Class returnType = method.getReturnType(); + if (returnType != String.class + && returnType != void.class + && returnType != Void.class + && returnType != PromptTemplate.class + && returnType != Agent.class + && returnType != Agent.Builder.class) { + throw new IllegalArgumentException("@AgentDef method " + method.getName() + " on " + + obj.getClass().getName() + + " must return String, PromptTemplate, Agent, Agent.Builder, or void; got " + + returnType.getSimpleName()); + } + Class[] params = method.getParameterTypes(); + if (params.length > 1 || (params.length == 1 && params[0] != Agent.Builder.class)) { + throw new IllegalArgumentException("@AgentDef method " + method.getName() + " on " + + obj.getClass().getName() + + " must take no parameters, or a single Agent.Builder to customize"); + } + } + + /** + * Invoke the agent method after the builder is pre-populated from the + * annotation, then build the agent. Dispatch is by declared return type: + * + *
      + *
    • {@code void}, no-arg — pure marker; never invoked.
    • + *
    • {@code void} + builder param — customizer; invoked once.
    • + *
    • {@code String}, no-arg — lazy dynamic instructions: the method is + * re-invoked every time {@link Agent#getInstructions()} resolves (each run + * submission), matching the Python SDK where callable instructions resolve + * at serialization time. A non-empty result wins over the annotation + * attribute.
    • + *
    • {@code String} + builder param — invoked once, eagerly: re-running a + * customizer per serialization would replay its side effects.
    • + *
    • {@code PromptTemplate} — invoked once; a non-null result becomes + * {@code instructionsTemplate}.
    • + *
    • {@code Agent.Builder} / {@code Agent} + builder param — the returned + * value is the definition (built if a builder).
    • + *
    + */ + private static Agent invokeAgentMethod(Object obj, Method method, AgentDef ann, Agent.Builder builder) { + Class returnType = method.getReturnType(); + boolean wantsBuilder = method.getParameterCount() == 1; + + if (returnType == String.class && !wantsBuilder) { + builder.instructions(() -> { + Object dynamic = invoke(obj, method); + return (dynamic instanceof String s && !s.isEmpty()) ? s : ann.instructions(); + }); + return builder.build(); + } + + Object result = (returnType == void.class || returnType == Void.class) && !wantsBuilder + ? null // pure marker — nothing to invoke + : (wantsBuilder ? invoke(obj, method, builder) : invoke(obj, method)); + + if (returnType == String.class) { + if (result instanceof String dynamic && !dynamic.isEmpty()) { + builder.instructions(dynamic); + } + } else if (returnType == PromptTemplate.class) { + if (result != null) { + builder.instructionsTemplate((PromptTemplate) result); + } + } else if (returnType == Agent.class || returnType == Agent.Builder.class) { + return buildFromFactoryResult(obj, method, result); + } + return builder.build(); + } + + /** Turn an {@code Agent}/{@code Agent.Builder} factory result into the agent. */ + private static Agent buildFromFactoryResult(Object obj, Method method, Object result) { + if (result == null) { + throw new IllegalArgumentException("@AgentDef factory method " + method.getName() + " on " + + obj.getClass().getName() + " returned null; it must return the agent definition"); + } + return result instanceof Agent.Builder b ? b.build() : (Agent) result; + } + + /** + * Reject non-default annotation attributes on a pure factory method (no-arg, + * returning {@code Agent} or {@code Agent.Builder}) — the factory builds the + * whole definition, so attributes other than {@code name} would be silently + * ignored. Methods that accept the pre-populated builder may use attributes. + */ + private static void requireDiscoveryOnlyAttributes(Object obj, Method method, AgentDef ann) { + List set = new ArrayList<>(); + if (!ann.model().isEmpty()) set.add("model"); + if (!ann.instructions().isEmpty()) set.add("instructions"); + if (!Arrays.equals(ann.tools(), new String[] {"*"})) set.add("tools"); + if (!Arrays.equals(ann.guardrails(), new String[] {"*"})) set.add("guardrails"); + if (ann.agents().length > 0) set.add("agents"); + if (ann.strategy() != org.conductoross.conductor.ai.enums.Strategy.HANDOFF) set.add("strategy"); + if (ann.maxTurns() != 25) set.add("maxTurns"); + if (ann.maxTokens() != 0) set.add("maxTokens"); + if (!Double.isNaN(ann.temperature())) set.add("temperature"); + if (ann.credentials().length > 0) set.add("credentials"); + if (ann.contextWindowBudget() != 0) set.add("contextWindowBudget"); + if (!set.isEmpty()) { + throw new IllegalArgumentException("@AgentDef factory method " + method.getName() + " on " + + obj.getClass().getName() + " returns " + + method.getReturnType().getSimpleName() + + " and builds the definition itself, but sets annotation attributes " + set + + " that would be ignored. Either drop the attributes, or accept the" + + " pre-populated Agent.Builder as a parameter."); + } + } + + /** Reflectively invoke the agent method, unwrapping reflection exceptions. */ + private static Object invoke(Object obj, Method method, Object... args) { + try { + method.setAccessible(true); + return method.invoke(obj, args); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Failed to invoke @AgentDef method " + method.getName(), e); + } + } + + /** + * Filter discovered definitions by the annotation's name list: + * {@code {"*"}} selects all, an empty array selects none, otherwise match by + * name and throw on unknown names. + */ + private static List selectByName( + List all, java.util.function.Function nameOf, String[] requested, String kind, Object obj) { + if (requested.length == 1 && "*".equals(requested[0])) { + return all; + } + List selected = new ArrayList<>(); + for (String want : requested) { + T match = all.stream() + .filter(t -> want.equals(nameOf.apply(t))) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No " + kind + " method named '" + want + + "' on " + obj.getClass().getName() + ". Available: " + + all.stream().map(nameOf).toList())); + selected.add(match); + } + return selected; + } +} diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/AgentAnnotationTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/AgentAnnotationTest.java new file mode 100644 index 000000000..da4afe4b0 --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/AgentAnnotationTest.java @@ -0,0 +1,532 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.conductoross.conductor.ai.annotations.AgentDef; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.GuardrailResult; +import org.conductoross.conductor.ai.model.ToolDef; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link AgentDef @AgentDef}-annotated + * method resolution via {@link Agent#fromInstance(Object)}. + */ +class AgentAnnotationTest { + + // ── Fixtures ───────────────────────────────────────────────────────── + + static class BasicAgent { + @AgentDef(model = "openai/gpt-4o", instructions = "You are a helpful assistant.") + public void assistant() {} + } + + static class DynamicInstructions { + @AgentDef(model = "openai/gpt-4o", instructions = "static fallback") + public String weatherbot() { + return "You are a weather assistant."; + } + } + + static class EmptyDynamicInstructions { + @AgentDef(model = "openai/gpt-4o", instructions = "static fallback") + public String agentWithFallback() { + return ""; + } + } + + static class WithTools { + @Tool(description = "Get weather for a city") + public String getWeather(String city) { + return "Sunny, 72F in " + city; + } + + @Tool(name = "get_time", description = "Get current time") + public String getTime() { + return "12:00"; + } + + @AgentDef(model = "openai/gpt-4o") + public String allTools() { + return "You can use all tools."; + } + + @AgentDef( + model = "openai/gpt-4o", + tools = {"get_time"}) + public String oneTool() { + return "You can tell the time."; + } + + @AgentDef( + model = "openai/gpt-4o", + tools = {}) + public String noTools() { + return "No tools for you."; + } + } + + static class UnknownToolName { + @Tool(description = "Get weather") + public String getWeather(String city) { + return "Sunny"; + } + + @AgentDef( + model = "openai/gpt-4o", + tools = {"does_not_exist"}) + public void broken() {} + } + + static class WithGuardrails { + @org.conductoross.conductor.ai.annotations.GuardrailDef + public GuardrailResult noPii(String output) { + return GuardrailResult.pass(); + } + + @AgentDef(model = "openai/gpt-4o") + public void guarded() {} + + @AgentDef( + model = "openai/gpt-4o", + guardrails = {}) + public void unguarded() {} + } + + static class MultiAgent { + @AgentDef(instructions = "Handle billing questions.") + public void billing() {} + + @AgentDef(model = "anthropic/claude-sonnet-4-6", instructions = "Handle technical support.") + public void support() {} + + @AgentDef( + model = "openai/gpt-4o", + instructions = "Route customer questions.", + agents = {"billing", "support"}, + strategy = Strategy.HANDOFF) + public void triage() {} + } + + static class UnknownSubAgent { + @AgentDef( + model = "openai/gpt-4o", + agents = {"ghost"}) + public void parent() {} + } + + static class CyclicAgents { + @AgentDef( + model = "openai/gpt-4o", + agents = {"b"}) + public void a() {} + + @AgentDef( + model = "openai/gpt-4o", + agents = {"a"}) + public void b() {} + } + + static class CustomAttributes { + @AgentDef( + name = "custom_name", + model = "openai/gpt-4o", + maxTurns = 5, + maxTokens = 1024, + temperature = 0.2, + credentials = {"OPENAI_API_KEY"}, + contextWindowBudget = 50000) + public void ignoredMethodName() {} + } + + static class BuilderCustomizer { + @Tool(description = "Search the web") + public String search(String query) { + return "results"; + } + + @AgentDef(model = "openai/gpt-4o", instructions = "You are a researcher.") + public void researcher(Agent.Builder builder) { + builder.termination(new org.conductoross.conductor.ai.termination.MaxMessageTermination(5)) + .synthesize(false); + } + + @AgentDef(model = "openai/gpt-4o", instructions = "static") + public String dynamicWithCustomizer(Agent.Builder builder) { + builder.maskedFields("ssn"); + return "dynamic instructions"; + } + } + + static class StrategyViaCustomizer { + @AgentDef(instructions = "Write a draft.") + public void writer() {} + + @AgentDef( + model = "openai/gpt-4o", + strategy = Strategy.SEQUENTIAL, + tools = {}) + public void pipeline(Agent.Builder builder) { + builder.agents( + Agent.fromInstance(this, "writer"), Agent.fromInstance(new CrossClassSpecialist(), "editor")); + } + } + + static class CrossClassSpecialist { + @AgentDef(model = "anthropic/claude-sonnet-4-6", instructions = "Edit the draft.") + public void editor() {} + } + + static class TwoParameters { + @AgentDef(model = "openai/gpt-4o") + public void bad(Agent.Builder builder, String extra) {} + } + + static class PromptTemplateReturn { + @AgentDef(model = "openai/gpt-4o") + public org.conductoross.conductor.ai.model.PromptTemplate templated() { + return new org.conductoross.conductor.ai.model.PromptTemplate( + "customer-support", java.util.Map.of("tone", "friendly")); + } + } + + static class FullAgentFactory { + @AgentDef + public Agent handbuilt() { + return Agent.builder() + .name("handbuilt") + .model("openai/gpt-4o") + .instructions("Factory built.") + .maxTurns(3) + .build(); + } + } + + static class FluentBuilderReturn { + @AgentDef(model = "openai/gpt-4o", instructions = "fluent") + public Agent.Builder fluent(Agent.Builder builder) { + return builder.maxTurns(3); + } + } + + static class NoArgBuilderFactory { + @AgentDef + public Agent.Builder scratch() { + return Agent.builder().name("scratch_agent").model("openai/gpt-4o"); + } + } + + static class FactoryWithAttributes { + @AgentDef(model = "openai/gpt-4o") + public Agent bad() { + return Agent.builder().name("x").build(); + } + } + + static class NullFactory { + @AgentDef + public Agent nothing() { + return null; + } + } + + static class LazyInstructions { + int calls = 0; + + @AgentDef(model = "openai/gpt-4o") + public String counterbot() { + calls++; + return "version " + calls; + } + } + + static class EagerWithBuilder { + int calls = 0; + + @AgentDef(model = "openai/gpt-4o") + public String eager(Agent.Builder builder) { + calls++; + return "eager " + calls; + } + } + + static class BadReturnType { + @AgentDef(model = "openai/gpt-4o") + public int badReturn() { + return 42; + } + } + + static class BadParameters { + @AgentDef(model = "openai/gpt-4o") + public String badParams(String unexpected) { + return "instructions"; + } + } + + // ── Tests ──────────────────────────────────────────────────────────── + + @Test + void basicAgentUsesMethodNameAndStaticInstructions() { + List agents = Agent.fromInstance(new BasicAgent()); + assertEquals(1, agents.size()); + Agent agent = agents.get(0); + assertEquals("assistant", agent.getName()); + assertEquals("openai/gpt-4o", agent.getModel()); + assertEquals("You are a helpful assistant.", agent.getInstructions()); + assertEquals(25, agent.getMaxTurns()); + } + + @Test + void stringReturningMethodProvidesDynamicInstructions() { + Agent agent = Agent.fromInstance(new DynamicInstructions(), "weatherbot"); + assertEquals("You are a weather assistant.", agent.getInstructions()); + } + + @Test + void emptyDynamicInstructionsFallBackToAttribute() { + Agent agent = Agent.fromInstance(new EmptyDynamicInstructions(), "agentWithFallback"); + assertEquals("static fallback", agent.getInstructions()); + } + + @Test + void allToolMethodsAttachedByDefault() { + Agent agent = Agent.fromInstance(new WithTools(), "allTools"); + assertEquals(2, agent.getTools().size()); + List names = agent.getTools().stream().map(ToolDef::getName).toList(); + assertTrue(names.contains("getWeather")); + assertTrue(names.contains("get_time")); + } + + @Test + void toolsFilteredByName() { + Agent agent = Agent.fromInstance(new WithTools(), "oneTool"); + assertEquals(1, agent.getTools().size()); + assertEquals("get_time", agent.getTools().get(0).getName()); + } + + @Test + void emptyToolsArrayAttachesNoTools() { + Agent agent = Agent.fromInstance(new WithTools(), "noTools"); + assertTrue(agent.getTools().isEmpty()); + } + + @Test + void unknownToolNameThrows() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new UnknownToolName())); + assertTrue(e.getMessage().contains("does_not_exist")); + } + + @Test + void guardrailsAttachedByDefaultAndFilterable() { + Agent guarded = Agent.fromInstance(new WithGuardrails(), "guarded"); + assertEquals(1, guarded.getGuardrails().size()); + assertEquals("noPii", guarded.getGuardrails().get(0).getName()); + + Agent unguarded = Agent.fromInstance(new WithGuardrails(), "unguarded"); + assertTrue(unguarded.getGuardrails().isEmpty()); + } + + @Test + void subAgentsResolvedByNameWithModelInheritance() { + Agent triage = Agent.fromInstance(new MultiAgent(), "triage"); + assertEquals(Strategy.HANDOFF, triage.getStrategy()); + assertEquals(2, triage.getAgents().size()); + + Agent billing = triage.getAgents().get(0); + assertEquals("billing", billing.getName()); + // billing has no model — inherits the parent's + assertEquals("openai/gpt-4o", billing.getModel()); + + Agent support = triage.getAgents().get(1); + // support declares its own model — no inheritance + assertEquals("anthropic/claude-sonnet-4-6", support.getModel()); + } + + @Test + void topLevelResolutionReturnsAllAgents() { + List agents = Agent.fromInstance(new MultiAgent()); + assertEquals(3, agents.size()); + } + + @Test + void unknownSubAgentNameThrows() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new UnknownSubAgent())); + assertTrue(e.getMessage().contains("ghost")); + } + + @Test + void cyclicSubAgentsThrow() { + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new CyclicAgents())); + } + + @Test + void annotationAttributesMapToAgentFields() { + Agent agent = Agent.fromInstance(new CustomAttributes(), "custom_name"); + assertEquals("custom_name", agent.getName()); + assertEquals(5, agent.getMaxTurns()); + assertEquals(1024, agent.getMaxTokens()); + assertEquals(0.2, agent.getTemperature()); + assertEquals(List.of("OPENAI_API_KEY"), agent.getCredentials()); + assertEquals(50000, agent.getContextWindowBudget()); + } + + @Test + void unsetOptionalsStayNull() { + Agent agent = Agent.fromInstance(new BasicAgent(), "assistant"); + assertNull(agent.getMaxTokens()); + assertNull(agent.getTemperature()); + assertNull(agent.getContextWindowBudget()); + } + + @Test + void customizerReceivesPrepopulatedBuilder() { + Agent agent = Agent.fromInstance(new BuilderCustomizer(), "researcher"); + // customizations from the method body + assertTrue(agent.getTermination() instanceof org.conductoross.conductor.ai.termination.MaxMessageTermination); + assertTrue(!agent.isSynthesize()); + // pre-populated state from the annotation survives + assertEquals("openai/gpt-4o", agent.getModel()); + assertEquals("You are a researcher.", agent.getInstructions()); + assertEquals(1, agent.getTools().size()); + assertEquals("search", agent.getTools().get(0).getName()); + } + + @Test + void returnedStringWinsOverCustomizerAndAttribute() { + Agent agent = Agent.fromInstance(new BuilderCustomizer(), "dynamicWithCustomizer"); + assertEquals("dynamic instructions", agent.getInstructions()); + assertEquals(List.of("ssn"), agent.getMaskedFields()); + } + + @Test + void strategyAppliesWhenSubAgentsAddedViaCustomizer() { + Agent pipeline = Agent.fromInstance(new StrategyViaCustomizer(), "pipeline"); + assertEquals(Strategy.SEQUENTIAL, pipeline.getStrategy()); + assertEquals(2, pipeline.getAgents().size()); + // cross-class sub-agent resolved from another instance + assertEquals("editor", pipeline.getAgents().get(1).getName()); + assertEquals("anthropic/claude-sonnet-4-6", pipeline.getAgents().get(1).getModel()); + } + + @Test + void extraParametersBeyondBuilderThrow() { + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new TwoParameters())); + } + + // ── Return-type ladder ─────────────────────────────────────────────── + + @Test + void promptTemplateReturnSetsInstructionsTemplate() { + Agent agent = Agent.fromInstance(new PromptTemplateReturn(), "templated"); + assertEquals("customer-support", agent.getInstructionsTemplate().getName()); + assertEquals("friendly", agent.getInstructionsTemplate().getVariables().get("tone")); + } + + @Test + void agentReturningMethodIsFullFactory() { + // discovery key is the method name; the agent keeps the name set by the factory + Agent agent = Agent.fromInstance(new FullAgentFactory(), "handbuilt"); + assertEquals("handbuilt", agent.getName()); + assertEquals("Factory built.", agent.getInstructions()); + assertEquals(3, agent.getMaxTurns()); + } + + @Test + void returnedBuilderIsBuiltWithAnnotationDefaults() { + Agent agent = Agent.fromInstance(new FluentBuilderReturn(), "fluent"); + assertEquals(3, agent.getMaxTurns()); + assertEquals("openai/gpt-4o", agent.getModel()); + assertEquals("fluent", agent.getInstructions()); + } + + @Test + void noArgBuilderReturnIsFactoryBuiltAsIs() { + Agent agent = Agent.fromInstance(new NoArgBuilderFactory(), "scratch"); + assertEquals("scratch_agent", agent.getName()); + assertEquals("openai/gpt-4o", agent.getModel()); + } + + @Test + void pureFactoryWithNonDefaultAttributesThrows() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new FactoryWithAttributes())); + assertTrue(e.getMessage().contains("model")); + } + + @Test + void factoryReturningNullThrows() { + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new NullFactory())); + } + + // ── Lazy instructions (evaluated per access, i.e. per run submission) ─ + + @Test + void noArgStringInstructionsAreLazyAndReevaluated() { + LazyInstructions fixture = new LazyInstructions(); + Agent agent = Agent.fromInstance(fixture, "counterbot"); + assertEquals(0, fixture.calls); + assertEquals("version 1", agent.getInstructions()); + assertEquals("version 2", agent.getInstructions()); + assertEquals(2, fixture.calls); + } + + @Test + void supplierInstructionsOnBuilderAreReevaluatedPerAccess() { + java.util.concurrent.atomic.AtomicInteger calls = new java.util.concurrent.atomic.AtomicInteger(); + Agent agent = Agent.builder() + .name("lazy") + .model("openai/gpt-4o") + .instructions(() -> "prompt v" + calls.incrementAndGet()) + .build(); + assertEquals("prompt v1", agent.getInstructions()); + assertEquals("prompt v2", agent.getInstructions()); + } + + @Test + void lazyInstructionsReachTheSerializedConfig() { + LazyInstructions fixture = new LazyInstructions(); + Agent agent = Agent.fromInstance(fixture, "counterbot"); + var serializer = new org.conductoross.conductor.ai.internal.AgentConfigSerializer(); + assertEquals("version 1", serializer.serialize(agent).get("instructions")); + assertEquals("version 2", serializer.serialize(agent).get("instructions")); + } + + @Test + void builderParamStringInstructionsStayEager() { + EagerWithBuilder fixture = new EagerWithBuilder(); + Agent agent = Agent.fromInstance(fixture, "eager"); + assertEquals(1, fixture.calls); + assertEquals("eager 1", agent.getInstructions()); + assertEquals("eager 1", agent.getInstructions()); + assertEquals(1, fixture.calls); + } + + @Test + void nonStringNonVoidReturnTypeThrows() { + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new BadReturnType())); + } + + @Test + void stringReturningMethodWithParametersThrows() { + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new BadParameters())); + } + + @Test + void missingNamedAgentThrows() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new BasicAgent(), "nope")); + assertTrue(e.getMessage().contains("nope")); + } +} From ca157ade25feef78a3613aac9d0d4a275751920a Mon Sep 17 00:00:00 2001 From: Dale Brady <49766562+bradyyie@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:08:37 -0400 Subject: [PATCH 19/61] Support Agentspan Embedded in Orkes Conductor (#273) --- cli/auth/auth0.go | 227 +++++++++++++ cli/auth/auth0_test.go | 181 ++++++++++ cli/auth/orkes.go | 60 ++++ cli/auth/orkes_test.go | 62 ++++ cli/auth/pkce.go | 174 ++++++++++ cli/auth/pkce_test.go | 159 +++++++++ cli/client/client.go | 98 +++--- cli/cmd/credentials.go | 7 +- cli/cmd/helpers.go | 10 + cli/cmd/login.go | 309 +++++++++++++++--- cli/cmd/login_test.go | 143 ++++---- cli/cmd/root.go | 2 +- cli/config/config.go | 26 ++ cli/config/token.go | 85 +++++ .../agentspan/agents/_internal/token_utils.py | 100 ++++++ .../agents/frameworks/claude_agent_sdk.py | 42 +-- .../agentspan/agents/frameworks/langchain.py | 7 +- .../agentspan/agents/frameworks/langgraph.py | 7 +- .../agentspan/agents/runtime/http_client.py | 61 ++-- .../src/agentspan/agents/runtime/runtime.py | 92 ++++-- sdk/python/tests/unit/test_sse_client.py | 46 ++- sdk/python/tests/unit/test_token_utils.py | 109 ++++++ .../credentials/CredentialEnvSeeder.java | 60 +--- .../skill/FileSystemSkillMetadataDAO.java | 221 +++++++++++++ .../service/SkillRegistryServiceTest.java | 21 +- .../runtime/compiler/MultiAgentCompiler.java | 4 +- .../runtime/compiler/ToolCompiler.java | 20 +- .../config/AgentSpanAutoConfiguration.java | 36 +- .../runtime/controller/SecretController.java | 2 + .../CredentialAwareHttpTaskConfig.java | 9 + .../credentials/KnownProviderEnvVars.java | 78 +++++ .../runtime/service/AgentHumanTaskConfig.java | 10 + .../runtime/service/AgentService.java | 65 +++- .../runtime/service/SkillRegistryService.java | 245 +++----------- .../runtime/spi/SkillMetadataDAO.java | 51 +++ .../dev/agentspan/runtime/tasks/Join.java | 2 + .../agentspan/runtime/util/EmbeddedMode.java | 31 ++ .../runtime/util/ProviderValidator.java | 15 +- 38 files changed, 2348 insertions(+), 529 deletions(-) create mode 100644 cli/auth/auth0.go create mode 100644 cli/auth/auth0_test.go create mode 100644 cli/auth/orkes.go create mode 100644 cli/auth/orkes_test.go create mode 100644 cli/auth/pkce.go create mode 100644 cli/auth/pkce_test.go create mode 100644 cli/config/token.go create mode 100644 sdk/python/src/agentspan/agents/_internal/token_utils.py create mode 100644 sdk/python/tests/unit/test_token_utils.py create mode 100644 server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/KnownProviderEnvVars.java create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/spi/SkillMetadataDAO.java create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/EmbeddedMode.java diff --git a/cli/auth/auth0.go b/cli/auth/auth0.go new file mode 100644 index 000000000..8dd20be38 --- /dev/null +++ b/cli/auth/auth0.go @@ -0,0 +1,227 @@ +// Copyright (c) 2025 AgentSpan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +// Package auth implements the Auth0 OAuth Device Authorization Grant (RFC 8628) +// for browser-based CLI login. This mirrors how the orkes-conductor UI authenticates: +// it obtains an Auth0-issued JWT and sends it to the backend as the X-Authorization +// header — there is no orkes-side token exchange. The device flow delegates the actual +// login to Auth0's hosted page, so it supports whatever the tenant allows +// (username/password, Google, SSO, MFA, ...). +package auth + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "time" +) + +// DefaultScope requests an OIDC token plus a refresh token. No audience is requested, +// matching the UI (which relies on the Auth0 tenant's default audience / ID token). +const DefaultScope = "openid profile email offline_access" + +// DeviceCode is the response from POST /oauth/device/code. +type DeviceCode struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURI string `json:"verification_uri"` + VerificationURIComplete string `json:"verification_uri_complete"` + ExpiresIn int `json:"expires_in"` + Interval int `json:"interval"` +} + +// Token is the response from POST /oauth/token. +type Token struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` +} + +// Auth0Config is the subset of the server's window.authConfig the CLI needs. +type Auth0Config struct { + Domain string + ClientID string + UseIDToken bool +} + +// RequestDeviceCode starts the device authorization flow. +func RequestDeviceCode(domain, clientID, scope string) (*DeviceCode, error) { + if scope == "" { + scope = DefaultScope + } + form := url.Values{"client_id": {clientID}, "scope": {scope}} + resp, err := http.PostForm(authURL(domain, "/oauth/device/code"), form) + if err != nil { + return nil, fmt.Errorf("request device code: %w", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("device code request failed (HTTP %d): %s", resp.StatusCode, string(body)) + } + var dc DeviceCode + if err := json.Unmarshal(body, &dc); err != nil { + return nil, fmt.Errorf("parse device code: %w", err) + } + if dc.Interval <= 0 { + dc.Interval = 5 + } + return &dc, nil +} + +// PollForToken polls the token endpoint until the user completes login in the browser, +// the code expires, or login is denied. Honors authorization_pending and slow_down. +func PollForToken(domain, clientID string, dc *DeviceCode) (*Token, error) { + deadline := time.Now().Add(time.Duration(dc.ExpiresIn) * time.Second) + interval := time.Duration(dc.Interval) * time.Second + for { + if time.Now().After(deadline) { + return nil, fmt.Errorf("device code expired before login completed") + } + time.Sleep(interval) + + form := url.Values{ + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + "device_code": {dc.DeviceCode}, + "client_id": {clientID}, + } + resp, err := http.PostForm(authURL(domain, "/oauth/token"), form) + if err != nil { + return nil, fmt.Errorf("poll token: %w", err) + } + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + var tok Token + if err := json.Unmarshal(body, &tok); err != nil { + return nil, fmt.Errorf("parse token: %w", err) + } + return &tok, nil + } + + var e struct { + Error string `json:"error"` + } + _ = json.Unmarshal(body, &e) + switch e.Error { + case "authorization_pending": + // keep waiting + case "slow_down": + interval += 5 * time.Second + case "expired_token": + return nil, fmt.Errorf("device code expired before login completed") + case "access_denied": + return nil, fmt.Errorf("login was denied") + default: + return nil, fmt.Errorf("token poll failed (HTTP %d): %s", resp.StatusCode, string(body)) + } + } +} + +// Refresh exchanges a refresh token for a fresh access/ID token. +func Refresh(domain, clientID, refreshToken string) (*Token, error) { + form := url.Values{ + "grant_type": {"refresh_token"}, + "client_id": {clientID}, + "refresh_token": {refreshToken}, + } + resp, err := http.PostForm(authURL(domain, "/oauth/token"), form) + if err != nil { + return nil, fmt.Errorf("refresh token: %w", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("refresh failed (HTTP %d): %s", resp.StatusCode, string(body)) + } + var tok Token + if err := json.Unmarshal(body, &tok); err != nil { + return nil, fmt.Errorf("parse refreshed token: %w", err) + } + return &tok, nil +} + +// DiscoverConfig fetches the server's /context.js and extracts the Auth0 config — the +// same runtime config the UI consumes, with the UI's exact resolution order +// (auth0-config.ts): authConfig.domain || auth0Identifiers.domain, same for clientId. +// Handles the real orkes serialization (quoted keys, e.g. {"clientId" : "..."}) as well +// as bare-key JS object literals. +func DiscoverConfig(serverBaseURL string) (*Auth0Config, error) { + base := strings.TrimRight(serverBaseURL, "/") + resp, err := http.Get(base + "/context.js") + if err != nil { + return nil, fmt.Errorf("fetch /context.js: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("/context.js returned HTTP %d", resp.StatusCode) + } + body, _ := io.ReadAll(resp.Body) + js := string(body) + + authBlock := sliceBlock(js, "window.authConfig") + idsBlock := sliceBlock(js, "window.auth0Identifiers") + + domain := extract(authBlock, "domain") + if domain == "" { + domain = extract(idsBlock, "domain") + } + clientID := extract(authBlock, "clientId") + if clientID == "" { + clientID = extract(idsBlock, "clientId") + } + + cfg := &Auth0Config{ + Domain: domain, + ClientID: clientID, + UseIDToken: useIDTokenTrueRe.MatchString(authBlock), + } + if cfg.Domain == "" || cfg.ClientID == "" { + return nil, fmt.Errorf("server has no Auth0 config in /context.js (is auth enabled?)") + } + return cfg, nil +} + +var useIDTokenTrueRe = regexp.MustCompile(`["']?useIdToken["']?\s*:\s*true`) + +// sliceBlock returns the {...} object literal assigned at `marker` (e.g. "window.authConfig"), +// or "" when absent. The blocks served by orkes/the UI are flat objects, so the first `}` +// after the marker closes the block. +func sliceBlock(js, marker string) string { + start := strings.Index(js, marker) + if start < 0 { + return "" + } + rest := js[start:] + end := strings.Index(rest, "}") + if end < 0 { + return rest + } + return rest[:end+1] +} + +// extract pulls a string value for `key` from a JS/JSON object literal, tolerating both +// quoted and bare keys and arbitrary spacing around the colon. +func extract(js, key string) string { + m := regexp.MustCompile(`["']?` + key + `["']?\s*:\s*["']([^"']+)["']`).FindStringSubmatch(js) + if len(m) == 2 { + return m[1] + } + return "" +} + +func authURL(domain, path string) string { + domain = strings.TrimRight(domain, "/") + if !strings.HasPrefix(domain, "http") { + domain = "https://" + domain + } + return domain + path +} diff --git a/cli/auth/auth0_test.go b/cli/auth/auth0_test.go new file mode 100644 index 000000000..67da174e6 --- /dev/null +++ b/cli/auth/auth0_test.go @@ -0,0 +1,181 @@ +package auth + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +// TestDeviceFlow drives RequestDeviceCode + PollForToken against a real local HTTP +// server emulating Auth0's device endpoints, including one authorization_pending poll. +func TestDeviceFlow(t *testing.T) { + polls := 0 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/oauth/device/code": + _ = json.NewEncoder(w).Encode(map[string]any{ + "device_code": "dev-123", + "user_code": "WXYZ-1234", + "verification_uri": "https://example/activate", + "verification_uri_complete": "https://example/activate?user_code=WXYZ-1234", + "expires_in": 300, + "interval": 1, + }) + case "/oauth/token": + polls++ + if polls < 2 { + w.WriteHeader(http.StatusForbidden) + _ = json.NewEncoder(w).Encode(map[string]string{"error": "authorization_pending"}) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "access_token": "access-xyz", + "id_token": "id-xyz", + "refresh_token": "refresh-xyz", + "expires_in": 3600, + "token_type": "Bearer", + }) + default: + http.NotFound(w, r) + } + })) + defer srv.Close() + + dc, err := RequestDeviceCode(srv.URL, "client-id", "") + if err != nil { + t.Fatalf("RequestDeviceCode: %v", err) + } + if dc.UserCode != "WXYZ-1234" { + t.Errorf("user code = %q", dc.UserCode) + } + + tok, err := PollForToken(srv.URL, "client-id", dc) + if err != nil { + t.Fatalf("PollForToken: %v", err) + } + if tok.AccessToken != "access-xyz" || tok.RefreshToken != "refresh-xyz" { + t.Errorf("unexpected token: %+v", tok) + } + if polls < 2 { + t.Errorf("expected at least 2 polls (one pending), got %d", polls) + } +} + +func TestRefresh(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "bad form", http.StatusBadRequest) + return + } + if r.FormValue("grant_type") != "refresh_token" || r.FormValue("refresh_token") != "rt" { + http.Error(w, "bad refresh", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"access_token": "new-access", "expires_in": 3600}) + })) + defer srv.Close() + + tok, err := Refresh(srv.URL, "client-id", "rt") + if err != nil { + t.Fatalf("Refresh: %v", err) + } + if tok.AccessToken != "new-access" { + t.Errorf("refreshed access token = %q", tok.AccessToken) + } +} + +func TestDiscoverConfig(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/context.js" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/javascript") + _, _ = w.Write([]byte(`window.authConfig = { type: "auth0", domain: "tenant.us.auth0.com", clientId: "abc123", useIdToken: true };`)) + })) + defer srv.Close() + + cfg, err := DiscoverConfig(srv.URL) + if err != nil { + t.Fatalf("DiscoverConfig: %v", err) + } + if cfg.Domain != "tenant.us.auth0.com" || cfg.ClientID != "abc123" || !cfg.UseIDToken { + t.Errorf("unexpected config: %+v", cfg) + } +} + +// TestDiscoverConfigRealOrkesFormat uses the exact serialization a real orkes server +// emits — quoted keys with spaces around the colon, plus the window.conductor flags +// block and the auth0Identifiers fallback block (regression: bare-key-only regex). +func TestDiscoverConfigRealOrkesFormat(t *testing.T) { + body := `window.conductor = { + "ENABLE_METRICS_DASHBOARD" : false, + "MULTITENANCY_TYPE" : "none" +}; + +window.authConfig = { + "useIdToken" : false, + "clientId" : "s4HLdVbnaJMGvPSgx2YLpynfJlW7GV2e", + "domain" : "auth.orkes.io", + "type" : "auth0" +}; + +window.auth0Identifiers = { + "clientId" : "s4HLdVbnaJMGvPSgx2YLpynfJlW7GV2e", + "domain" : "auth.orkes.io" +};` + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/context.js" { + http.NotFound(w, r) + return + } + _, _ = w.Write([]byte(body)) + })) + defer srv.Close() + + cfg, err := DiscoverConfig(srv.URL) + if err != nil { + t.Fatalf("DiscoverConfig: %v", err) + } + if cfg.Domain != "auth.orkes.io" || cfg.ClientID != "s4HLdVbnaJMGvPSgx2YLpynfJlW7GV2e" { + t.Errorf("unexpected config: %+v", cfg) + } + if cfg.UseIDToken { + t.Error("useIdToken=false in authConfig but parsed true") + } +} + +// TestDiscoverConfigIdentifiersFallback mirrors the UI's auth0-config.ts fallback: +// authConfig missing -> values from window.auth0Identifiers. +func TestDiscoverConfigIdentifiersFallback(t *testing.T) { + body := `window.auth0Identifiers = { + "clientId" : "cid-fallback", + "domain" : "tenant.eu.auth0.com" +};` + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(body)) + })) + defer srv.Close() + + cfg, err := DiscoverConfig(srv.URL) + if err != nil { + t.Fatalf("DiscoverConfig: %v", err) + } + if cfg.Domain != "tenant.eu.auth0.com" || cfg.ClientID != "cid-fallback" { + t.Errorf("unexpected config: %+v", cfg) + } +} + +func TestDiscoverConfigNoAuth(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`window.conductor = { stack: "test" };`)) + })) + defer srv.Close() + + if _, err := DiscoverConfig(srv.URL); err == nil { + t.Fatal("expected error when /context.js has no authConfig") + } +} diff --git a/cli/auth/orkes.go b/cli/auth/orkes.go new file mode 100644 index 000000000..466c65dca --- /dev/null +++ b/cli/auth/orkes.go @@ -0,0 +1,60 @@ +// Copyright (c) 2025 AgentSpan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package auth + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +// MintOrkesToken exchanges an orkes application access key (keyId/keySecret) for a JWT +// via POST {server}/api/token — orkes' internal/service-account auth path (no external IdP). +// Returns the JWT and its expiry (unix seconds, decoded from the token's exp claim). +func MintOrkesToken(serverBase, keyID, keySecret string) (token string, expiresAt int64, err error) { + base := strings.TrimRight(serverBase, "/") + body, _ := json.Marshal(map[string]string{"keyId": keyID, "keySecret": keySecret}) + resp, err := http.Post(base+"/api/token", "application/json", bytes.NewReader(body)) + if err != nil { + return "", 0, fmt.Errorf("mint token: %w", err) + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return "", 0, fmt.Errorf("token request failed (HTTP %d): %s", resp.StatusCode, strings.TrimSpace(string(b))) + } + var r struct { + Token string `json:"token"` + } + if json.Unmarshal(b, &r) != nil || r.Token == "" { + return "", 0, fmt.Errorf("no token in response: %s", strings.TrimSpace(string(b))) + } + return r.Token, TokenExp(r.Token), nil +} + +// TokenExp decodes the `exp` (unix seconds) claim from a JWT without verifying the signature. +// Returns 0 when the token is opaque or has no exp. +func TokenExp(jwt string) int64 { + parts := strings.Split(jwt, ".") + if len(parts) < 2 { + return 0 + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + if payload, err = base64.URLEncoding.DecodeString(parts[1]); err != nil { + return 0 + } + } + var claims struct { + Exp int64 `json:"exp"` + } + if json.Unmarshal(payload, &claims) != nil { + return 0 + } + return claims.Exp +} diff --git a/cli/auth/orkes_test.go b/cli/auth/orkes_test.go new file mode 100644 index 000000000..0bbcc9f43 --- /dev/null +++ b/cli/auth/orkes_test.go @@ -0,0 +1,62 @@ +package auth + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestMintOrkesToken(t *testing.T) { + header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS512","typ":"JWT"}`)) + payload := base64.RawURLEncoding.EncodeToString([]byte(`{"exp":4102444800,"orkes_conductor_token":true}`)) + jwt := header + "." + payload + ".sig" + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/token" || r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + var body map[string]string + _ = json.NewDecoder(r.Body).Decode(&body) + if body["keyId"] != "kid" || body["keySecret"] != "ksecret" { + http.Error(w, "bad creds", http.StatusUnauthorized) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{"token": jwt}) + })) + defer srv.Close() + + token, exp, err := MintOrkesToken(srv.URL, "kid", "ksecret") + if err != nil { + t.Fatalf("MintOrkesToken: %v", err) + } + if token != jwt { + t.Errorf("token mismatch") + } + if exp != 4102444800 { + t.Errorf("exp = %d, want 4102444800", exp) + } +} + +func TestMintOrkesTokenBadCreds(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "unauthorized", http.StatusUnauthorized) + })) + defer srv.Close() + if _, _, err := MintOrkesToken(srv.URL, "x", "y"); err == nil { + t.Fatal("expected error on 401") + } +} + +func TestTokenExp(t *testing.T) { + payload := base64.RawURLEncoding.EncodeToString([]byte(`{"exp":1700000000}`)) + if got := TokenExp("h." + payload + ".s"); got != 1700000000 { + t.Errorf("TokenExp = %d, want 1700000000", got) + } + if got := TokenExp("opaque-token"); got != 0 { + t.Errorf("TokenExp(opaque) = %d, want 0", got) + } +} diff --git a/cli/auth/pkce.go b/cli/auth/pkce.go new file mode 100644 index 000000000..80994b945 --- /dev/null +++ b/cli/auth/pkce.go @@ -0,0 +1,174 @@ +// Copyright (c) 2025 AgentSpan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package auth + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// PKCE implements the OAuth 2.0 Authorization Code flow with PKCE (RFC 7636) over a +// loopback redirect (RFC 8252 §7.3) — the browser-based login a native CLI uses when the +// Device Authorization grant is not enabled on the client. It reuses the SAME public +// clientId the orkes UI uses (authorization_code + PKCE is what the UI itself runs); +// the only requirement is that the loopback redirect URI is in the Auth0 application's +// Allowed Callback URLs (the local UI origin, e.g. http://localhost:5001, already is). + +// GeneratePKCE returns a code_verifier and its S256 code_challenge. +func GeneratePKCE() (verifier, challenge string, err error) { + buf := make([]byte, 32) + if _, err = rand.Read(buf); err != nil { + return "", "", fmt.Errorf("generate PKCE verifier: %w", err) + } + verifier = base64.RawURLEncoding.EncodeToString(buf) + sum := sha256.Sum256([]byte(verifier)) + challenge = base64.RawURLEncoding.EncodeToString(sum[:]) + return verifier, challenge, nil +} + +// RandomState returns a random opaque state value for CSRF protection. +func RandomState() (string, error) { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(buf), nil +} + +// BuildAuthorizeURL constructs the Auth0 /authorize URL for the code+PKCE flow. +func BuildAuthorizeURL(domain, clientID, redirectURI, scope, state, challenge string) string { + if scope == "" { + scope = DefaultScope + } + q := url.Values{ + "client_id": {clientID}, + "response_type": {"code"}, + "redirect_uri": {redirectURI}, + "scope": {scope}, + "state": {state}, + "code_challenge": {challenge}, + "code_challenge_method": {"S256"}, + } + return authURL(domain, "/authorize") + "?" + q.Encode() +} + +// CodeCapture is a bound loopback listener awaiting the OAuth redirect. +type CodeCapture struct { + srv *http.Server + resCh chan captureResult +} + +type captureResult struct { + code string + err error +} + +// StartCodeCapture binds the loopback host:port of redirectURI immediately — failing fast +// if the port is busy (e.g. the UI dev server holds it) BEFORE any browser is opened — and +// starts serving the callback. Call Wait to block for the code. +func StartCodeCapture(redirectURI, expectedState string) (*CodeCapture, error) { + u, err := url.Parse(redirectURI) + if err != nil { + return nil, fmt.Errorf("parse redirect uri: %w", err) + } + if u.Port() == "" { + return nil, fmt.Errorf("redirect uri must include an explicit port (got %q)", redirectURI) + } + path := u.Path + if path == "" { + path = "/" + } + + ln, err := net.Listen("tcp", u.Host) + if err != nil { + return nil, fmt.Errorf( + "cannot bind %s — is something (the UI dev server?) running on that port? "+ + "Stop it during login or pass a different whitelisted --redirect-uri (%w)", u.Host, err) + } + + resCh := make(chan captureResult, 1) + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Accept the callback on the redirect path (or root) only. + if r.URL.Path != path && r.URL.Path != "/" { + http.NotFound(w, r) + return + } + q := r.URL.Query() + if e := q.Get("error"); e != "" { + http.Error(w, "Login failed: "+e, http.StatusBadRequest) + resCh <- captureResult{err: fmt.Errorf("authorization failed: %s (%s)", e, q.Get("error_description"))} + return + } + code := q.Get("code") + if code == "" { + // Not the OAuth callback (e.g. favicon) — ignore. + http.NotFound(w, r) + return + } + if q.Get("state") != expectedState { + http.Error(w, "State mismatch", http.StatusBadRequest) + resCh <- captureResult{err: fmt.Errorf("state mismatch in callback")} + return + } + w.Header().Set("Content-Type", "text/html") + _, _ = io.WriteString(w, "

    Login complete.

    You can close this tab and return to the terminal.") + resCh <- captureResult{code: code} + }) + + srv := &http.Server{Handler: mux} + go func() { _ = srv.Serve(ln) }() + return &CodeCapture{srv: srv, resCh: resCh}, nil +} + +// Wait blocks until the redirect delivers a code, an OAuth error arrives, or timeout. +// The listener is shut down before returning. +func (c *CodeCapture) Wait(timeout time.Duration) (string, error) { + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + _ = c.srv.Shutdown(ctx) + }() + select { + case r := <-c.resCh: + return r.code, r.err + case <-time.After(timeout): + return "", fmt.Errorf("timed out waiting for the browser login (after %s)", timeout) + } +} + +// ExchangeCode swaps an authorization code + PKCE verifier for tokens (public client). +func ExchangeCode(domain, clientID, code, verifier, redirectURI string) (*Token, error) { + form := url.Values{ + "grant_type": {"authorization_code"}, + "client_id": {clientID}, + "code": {code}, + "code_verifier": {verifier}, + "redirect_uri": {redirectURI}, + } + resp, err := http.PostForm(authURL(domain, "/oauth/token"), form) + if err != nil { + return nil, fmt.Errorf("exchange code: %w", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("code exchange failed (HTTP %d): %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + var tok Token + if err := json.Unmarshal(body, &tok); err != nil { + return nil, fmt.Errorf("parse token: %w", err) + } + return &tok, nil +} diff --git a/cli/auth/pkce_test.go b/cli/auth/pkce_test.go new file mode 100644 index 000000000..3c1d07398 --- /dev/null +++ b/cli/auth/pkce_test.go @@ -0,0 +1,159 @@ +package auth + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" +) + +func TestGeneratePKCE(t *testing.T) { + verifier, challenge, err := GeneratePKCE() + if err != nil { + t.Fatalf("GeneratePKCE: %v", err) + } + sum := sha256.Sum256([]byte(verifier)) + want := base64.RawURLEncoding.EncodeToString(sum[:]) + if challenge != want { + t.Errorf("challenge mismatch: got %q want %q", challenge, want) + } + if len(verifier) < 43 { // 32 bytes base64url = 43 chars, RFC 7636 minimum + t.Errorf("verifier too short: %d", len(verifier)) + } +} + +func TestBuildAuthorizeURL(t *testing.T) { + u, err := url.Parse(BuildAuthorizeURL("tenant.auth0.com", "cid", "http://localhost:5001", "", "st8", "ch4ll")) + if err != nil { + t.Fatalf("parse: %v", err) + } + if u.Host != "tenant.auth0.com" || u.Path != "/authorize" { + t.Errorf("unexpected url: %s", u) + } + q := u.Query() + for k, want := range map[string]string{ + "client_id": "cid", + "response_type": "code", + "redirect_uri": "http://localhost:5001", + "state": "st8", + "code_challenge": "ch4ll", + "code_challenge_method": "S256", + "scope": DefaultScope, + } { + if got := q.Get(k); got != want { + t.Errorf("%s = %q, want %q", k, got, want) + } + } +} + +// freePort grabs an ephemeral port and releases it for the capture server to rebind. +func freePort(t *testing.T) int { + t.Helper() + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + port := ln.Addr().(*net.TCPAddr).Port + _ = ln.Close() + return port +} + +func TestCodeCapture(t *testing.T) { + port := freePort(t) + redirect := fmt.Sprintf("http://127.0.0.1:%d", port) + + capture, err := StartCodeCapture(redirect, "state-1") + if err != nil { + t.Fatalf("StartCodeCapture: %v", err) + } + + // Simulate the browser redirect. + go func() { + resp, gerr := http.Get(fmt.Sprintf("%s/?code=auth-code-42&state=state-1", redirect)) + if gerr == nil { + resp.Body.Close() + } + }() + + code, err := capture.Wait(10 * time.Second) + if err != nil { + t.Fatalf("Wait: %v", err) + } + if code != "auth-code-42" { + t.Errorf("code = %q", code) + } +} + +func TestCodeCaptureStateMismatch(t *testing.T) { + port := freePort(t) + redirect := fmt.Sprintf("http://127.0.0.1:%d", port) + + capture, err := StartCodeCapture(redirect, "expected") + if err != nil { + t.Fatalf("StartCodeCapture: %v", err) + } + + go func() { + resp, gerr := http.Get(fmt.Sprintf("%s/?code=c&state=WRONG", redirect)) + if gerr == nil { + resp.Body.Close() + } + }() + + if _, err := capture.Wait(10 * time.Second); err == nil { + t.Fatal("expected state-mismatch error") + } +} + +func TestStartCodeCapturePortBusy(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + defer ln.Close() + busy := fmt.Sprintf("http://127.0.0.1:%d", ln.Addr().(*net.TCPAddr).Port) + + // Bind must fail immediately — BEFORE any browser would be opened. + if _, err := StartCodeCapture(busy, "s"); err == nil { + t.Fatal("expected bind error on busy port") + } +} + +func TestExchangeCode(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/oauth/token" { + http.NotFound(w, r) + return + } + if err := r.ParseForm(); err != nil { + http.Error(w, "bad form", http.StatusBadRequest) + return + } + if r.FormValue("grant_type") != "authorization_code" || + r.FormValue("code") != "the-code" || + r.FormValue("code_verifier") != "the-verifier" || + r.FormValue("redirect_uri") != "http://localhost:5001" { + http.Error(w, "bad exchange params", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "access_token": "at", "id_token": "idt", "refresh_token": "rt", "expires_in": 3600, + }) + })) + defer srv.Close() + + tok, err := ExchangeCode(srv.URL, "cid", "the-code", "the-verifier", "http://localhost:5001") + if err != nil { + t.Fatalf("ExchangeCode: %v", err) + } + if tok.AccessToken != "at" || tok.RefreshToken != "rt" { + t.Errorf("unexpected token: %+v", tok) + } +} diff --git a/cli/client/client.go b/cli/client/client.go index 37b0827a7..7f23eee58 100644 --- a/cli/client/client.go +++ b/cli/client/client.go @@ -15,6 +15,7 @@ import ( "strings" "time" + "github.com/agentspan-ai/agentspan/cli/auth" "github.com/agentspan-ai/agentspan/cli/config" ) @@ -22,14 +23,61 @@ type Client struct { baseURL string httpClient *http.Client apiKey string + authToken string // Auth0 JWT sent as X-Authorization (from ~/.agentspan/token) } func New(cfg *config.Config) *Client { - return &Client{ + c := &Client{ baseURL: strings.TrimRight(cfg.ServerURL, "/"), httpClient: &http.Client{Timeout: 30 * time.Second}, apiKey: cfg.APIKey, } + c.authToken = resolveAuthToken() + return c +} + +// resolveAuthToken loads the stored login token, refreshing it via Auth0 if expired, +// and returns the JWT to send as X-Authorization. Empty when not logged in. +func resolveAuthToken() string { + t, err := config.LoadToken() + if err != nil || t == nil { + return "" + } + if t.Expired() { + switch { + case t.RefreshToken != "" && t.Auth0Domain != "" && t.ClientID != "": + // Auth0 device-flow token: refresh with the refresh token. + if nt, rerr := auth.Refresh(t.Auth0Domain, t.ClientID, t.RefreshToken); rerr == nil { + t.AccessToken = nt.AccessToken + if nt.IDToken != "" { + t.IDToken = nt.IDToken + } + if nt.RefreshToken != "" { + t.RefreshToken = nt.RefreshToken + } + t.ExpiresAt = time.Now().Add(time.Duration(nt.ExpiresIn) * time.Second).Unix() + _ = config.SaveToken(t) + } + case t.KeyID != "" && t.KeySecret != "" && t.ServerURL != "": + // orkes access-key token: re-mint via POST /api/token. + if tok, exp, merr := auth.MintOrkesToken(t.ServerURL, t.KeyID, t.KeySecret); merr == nil { + t.AccessToken = tok + t.ExpiresAt = exp + _ = config.SaveToken(t) + } + } + } + return t.Header() +} + +// applyAuth attaches the auth header: X-Authorization (Auth0 JWT) when logged in, +// else a legacy Authorization: Bearer from a configured API key. orkes accepts both. +func (c *Client) applyAuth(req *http.Request) { + if c.authToken != "" { + req.Header.Set("X-Authorization", c.authToken) + } else if c.apiKey != "" { + req.Header.Set("Authorization", "Bearer "+c.apiKey) + } } func (c *Client) doRequest(method, path string, body interface{}) (*http.Response, error) { @@ -49,9 +97,7 @@ func (c *Client) doRequest(method, path string, body interface{}) (*http.Respons if body != nil { req.Header.Set("Content-Type", "application/json") } - if c.apiKey != "" { - req.Header.Set("Authorization", "Bearer "+c.apiKey) - } + c.applyAuth(req) resp, err := c.httpClient.Do(req) if err != nil { @@ -93,9 +139,7 @@ func (c *Client) doMultipartRequest(path string, manifest []byte, packageBytes [ return nil, err } req.Header.Set("Content-Type", writer.FormDataContentType()) - if c.apiKey != "" { - req.Header.Set("Authorization", "Bearer "+c.apiKey) - } + c.applyAuth(req) resp, err := c.httpClient.Do(req) if err != nil { @@ -196,9 +240,7 @@ func (c *Client) PollTask(taskType string) (map[string]interface{}, error) { if err != nil { return nil, err } - if c.apiKey != "" { - req.Header.Set("Authorization", "Bearer "+c.apiKey) - } + c.applyAuth(req) resp, err := c.httpClient.Do(req) if err != nil { @@ -578,9 +620,7 @@ func (c *Client) Stream(executionID string, lastEventID string, events chan<- SS if lastEventID != "" { req.Header.Set("Last-Event-ID", lastEventID) } - if c.apiKey != "" { - req.Header.Set("Authorization", "Bearer "+c.apiKey) - } + c.applyAuth(req) resp, err := streamClient.Do(req) if err != nil { @@ -645,34 +685,6 @@ func sseFieldValue(line, prefix string) string { // ─── Auth API ───────────────────────────────────────────────────────────────── -// LoginRequest is the payload for POST /api/auth/login -type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -// LoginResponse carries the JWT returned by the server -type LoginResponse struct { - Token string `json:"token"` -} - -// Login authenticates with the server and returns a JWT. -func (c *Client) Login(username, password string) (*LoginResponse, error) { - resp, err := c.doRequest("POST", "/api/auth/login", &LoginRequest{ - Username: username, - Password: password, - }) - if err != nil { - return nil, err - } - defer resp.Body.Close() - var result LoginResponse - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, fmt.Errorf("decode login response: %w", err) - } - return &result, nil -} - // ─── Credentials management API ─────────────────────────────────────────────────── // CredentialMeta is the list-view for a stored credential (from GET /api/secrets/v2). @@ -706,9 +718,7 @@ func (c *Client) SetCredential(name, value string) error { return err } req.Header.Set("Content-Type", "text/plain") - if c.apiKey != "" { - req.Header.Set("Authorization", "Bearer "+c.apiKey) - } + c.applyAuth(req) resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("request failed: %w", err) diff --git a/cli/cmd/credentials.go b/cli/cmd/credentials.go index 0f722c40d..6db14502f 100644 --- a/cli/cmd/credentials.go +++ b/cli/cmd/credentials.go @@ -6,7 +6,6 @@ import ( "text/tabwriter" "github.com/agentspan-ai/agentspan/cli/client" - "github.com/agentspan-ai/agentspan/cli/config" "github.com/fatih/color" "github.com/spf13/cobra" ) @@ -37,7 +36,7 @@ var secretsSetCmd = &cobra.Command{ } func runSecretsSet(name, value string) error { - cfg := config.Load() + cfg := getConfig() c := client.New(cfg) return c.SetCredential(name, value) } @@ -58,7 +57,7 @@ var secretsListCmd = &cobra.Command{ } func runSecretsList() (string, error) { - cfg := config.Load() + cfg := getConfig() c := client.New(cfg) secrets, err := c.ListCredentials() if err != nil { @@ -94,7 +93,7 @@ var secretsDeleteCmd = &cobra.Command{ } func runSecretsDelete(name string) error { - cfg := config.Load() + cfg := getConfig() return client.New(cfg).DeleteCredential(name) } diff --git a/cli/cmd/helpers.go b/cli/cmd/helpers.go index 7c31a7ff1..bc2a02fd7 100644 --- a/cli/cmd/helpers.go +++ b/cli/cmd/helpers.go @@ -4,6 +4,9 @@ package cmd import ( + "fmt" + "os" + "github.com/agentspan-ai/agentspan/cli/client" "github.com/agentspan-ai/agentspan/cli/config" ) @@ -12,6 +15,13 @@ func getConfig() *config.Config { cfg := config.Load() if serverURL != "" { cfg.ServerURL = serverURL + // An explicitly passed --server becomes the default for subsequent commands. + // Notice goes to stderr so piped stdout (JSON output etc.) stays clean. + if config.FileServerURL() != serverURL { + if err := config.SaveDefaultServer(serverURL); err == nil { + fmt.Fprintf(os.Stderr, "Default server set to %s (%s)\n", serverURL, config.ConfigDir()) + } + } } return cfg } diff --git a/cli/cmd/login.go b/cli/cmd/login.go index 3f25b9f5e..ad6796cc6 100644 --- a/cli/cmd/login.go +++ b/cli/cmd/login.go @@ -1,99 +1,308 @@ +// Copyright (c) 2025 AgentSpan +// Licensed under the MIT License. See LICENSE file in the project root for details. + package cmd import ( "bufio" "fmt" + "io" "os" + "os/exec" + "runtime" "strings" - "syscall" + "time" - "github.com/agentspan-ai/agentspan/cli/client" + "github.com/agentspan-ai/agentspan/cli/auth" "github.com/agentspan-ai/agentspan/cli/config" "github.com/fatih/color" "github.com/spf13/cobra" "golang.org/x/term" ) +var ( + loginDomain string + loginClientID string + loginScope string + loginNoOpen bool + loginKeyID string + loginKeySecret string + loginDevice bool + loginRedirectURI string +) + var loginCmd = &cobra.Command{ Use: "login", - Short: "Log in to the AgentSpan server and store an auth token", - Long: `Prompts for username and password, authenticates against the server, -and stores the returned JWT in ~/.agentspan/config.json. + Short: "Log in via your browser (Auth0) and store the token", + Long: `Logs in through your browser using the same Auth0 application the orkes-conductor +UI uses (Authorization Code + PKCE with a loopback redirect): your browser opens the +hosted login page (username/password, Google, SSO — whatever the tenant allows), the +redirect is captured locally, and the resulting token is stored in ~/.agentspan/token +and sent to the server as the X-Authorization header on every request. + +The Auth0 domain and client id are auto-discovered from the server's /context.js (the +same runtime config the UI consumes). Override with --auth0-domain / --auth0-client-id. + +Alternative grants: + --device OAuth Device Authorization flow (headless/SSH machines; + requires the Device Code grant enabled on the Auth0 app) + --key-id / --key-secret orkes application access key (service accounts / CI) -On localhost with auth disabled, this command is not required — the server -accepts all requests as anonymous admin automatically.`, +On a localhost server with auth disabled, login is not required — requests are accepted +as anonymous admin automatically.`, RunE: func(cmd *cobra.Command, args []string) error { cfg := getConfig() - if cfg.IsLocalhost() && cfg.APIKey == "" { - color.Yellow("Server is localhost — auth is optional.") - fmt.Println("Proceeding without login (anonymous admin mode).") - return nil + // No --server flag and no env override: ask which instance to log into + // (pre-filled with the current default; plain Enter keeps it). TTY only. + if serverURL == "" && + os.Getenv("AGENTSPAN_SERVER_URL") == "" && os.Getenv("AGENT_SERVER_URL") == "" && + term.IsTerminal(int(os.Stdin.Fd())) { + if entered := promptServerURL(os.Stdin, cfg.ServerURL); entered != "" { + cfg.ServerURL = entered + } } - fmt.Print("Username: ") - reader := bufio.NewReader(os.Stdin) - username, err := reader.ReadString('\n') - if err != nil { - return fmt.Errorf("read username: %w", err) + keyID := loginKeyID + if keyID == "" { + keyID = os.Getenv("AGENTSPAN_AUTH_KEY") } - username = strings.TrimSpace(username) - - fmt.Print("Password: ") - passwordBytes, err := term.ReadPassword(int(syscall.Stdin)) - fmt.Println() - if err != nil { - return fmt.Errorf("read password: %w", err) + keySecret := loginKeySecret + if keySecret == "" { + keySecret = os.Getenv("AGENTSPAN_AUTH_SECRET") } - password := string(passwordBytes) - if err := doLogin(cfg, username, password); err != nil { + var err error + if keyID != "" && keySecret != "" { + err = loginWithKey(cfg, keyID, keySecret) + } else { + err = loginWithAuth0(cfg) + } + if err != nil { return err } - color.Green("Logged in successfully.") - fmt.Printf("Token stored in %s/config.json\n", config.ConfigDir()) + // A successful login pins this server as the default for subsequent commands. + if config.FileServerURL() != cfg.ServerURL { + if serr := config.SaveDefaultServer(cfg.ServerURL); serr == nil { + color.Green("Default server set to %s", cfg.ServerURL) + } + } return nil }, } +// promptServerURL asks for the server URL, showing the current default. Returns the +// entered URL ("" = keep current). A missing scheme defaults to http:// (local instances). +func promptServerURL(r io.Reader, current string) string { + fmt.Printf("Server URL [%s]: ", current) + line, _ := bufio.NewReader(r).ReadString('\n') + s := strings.TrimSpace(line) + if s == "" { + return "" + } + if !strings.Contains(s, "://") { + s = "http://" + s + } + return strings.TrimRight(s, "/") +} + +// loginWithKey authenticates via an orkes application access key (keyId/keySecret) — +// the internal / service-account path: POST /api/token -> JWT. The key/secret are stored +// (0600) so the client can re-mint the token when it expires. +func loginWithKey(cfg *config.Config, keyID, keySecret string) error { + token, exp, err := auth.MintOrkesToken(cfg.ServerURL, keyID, keySecret) + if err != nil { + return err + } + if err := config.SaveToken(&config.TokenInfo{ + AccessToken: token, + ExpiresAt: exp, + KeyID: keyID, + KeySecret: keySecret, + ServerURL: cfg.ServerURL, + }); err != nil { + return fmt.Errorf("store token: %w", err) + } + color.Green("Logged in with access key. Token stored in %s", config.TokenPath()) + return nil +} + +// loginWithAuth0 runs the Auth0 device authorization grant (browser-based, matching the UI). +func loginWithAuth0(cfg *config.Config) error { + domain, clientID, useIDToken := loginDomain, loginClientID, false + if domain == "" || clientID == "" { + discovered, err := auth.DiscoverConfig(cfg.ServerURL) + if err != nil { + return fmt.Errorf( + "could not discover Auth0 config from %s (%w).\n"+ + "Pass --auth0-domain/--auth0-client-id, use --key-id/--key-secret, or confirm the server has auth enabled", + cfg.ServerURL, err) + } + if domain == "" { + domain = discovered.Domain + } + if clientID == "" { + clientID = discovered.ClientID + } + useIDToken = discovered.UseIDToken + } + + if loginDevice { + return loginWithDeviceFlow(domain, clientID, useIDToken) + } + // Default: browser Authorization Code + PKCE — the same grant the UI uses, so it + // works with the stock UI clientId (device flow requires a tenant-side grant toggle). + return loginWithBrowserPKCE(domain, clientID, useIDToken) +} + +// loginWithDeviceFlow runs the OAuth Device Authorization grant (for headless machines). +// Requires the Device Code grant to be enabled on the Auth0 application. +func loginWithDeviceFlow(domain, clientID string, useIDToken bool) error { + dc, err := auth.RequestDeviceCode(domain, clientID, loginScope) + if err != nil { + if strings.Contains(err.Error(), "unauthorized_client") { + return fmt.Errorf( + "the Device Authorization grant is not enabled on this Auth0 client.\n"+ + "Run without --device to use the browser login, or enable the Device Code grant "+ + "on the application in the Auth0 dashboard (%w)", err) + } + return err + } + + verifyURL := dc.VerificationURIComplete + if verifyURL == "" { + verifyURL = dc.VerificationURI + } + fmt.Println() + color.Cyan("To log in, open this URL in your browser:") + fmt.Printf(" %s\n", verifyURL) + color.Cyan("Confirm this code is shown: %s", dc.UserCode) + fmt.Println() + if !loginNoOpen { + _ = openBrowser(verifyURL) // best-effort; URL is printed regardless + } + fmt.Println("Waiting for you to complete login in the browser...") + + tok, err := auth.PollForToken(domain, clientID, dc) + if err != nil { + return err + } + return saveAuth0Token(tok, useIDToken, domain, clientID) +} + +// loginWithBrowserPKCE runs the Authorization Code + PKCE flow with a loopback redirect +// (RFC 8252): bind the redirect URI locally, open the hosted login in the browser, capture +// the ?code= from the redirect, and exchange it with the PKCE verifier. Works with the same +// public clientId the UI uses — the redirect URI must exactly match one of the Auth0 app's +// Allowed Callback URLs (the local UI origin, e.g. http://localhost:5001, is whitelisted). +func loginWithBrowserPKCE(domain, clientID string, useIDToken bool) error { + verifier, challenge, err := auth.GeneratePKCE() + if err != nil { + return err + } + state, err := auth.RandomState() + if err != nil { + return err + } + authorizeURL := auth.BuildAuthorizeURL(domain, clientID, loginRedirectURI, loginScope, state, challenge) + + // Bind the loopback port BEFORE printing anything or opening the browser — + // fail fast if it's busy (e.g. the UI dev server holds it). + capture, err := auth.StartCodeCapture(loginRedirectURI, state) + if err != nil { + return err + } + + fmt.Println() + color.Cyan("To log in, open this URL in your browser:") + fmt.Printf(" %s\n", authorizeURL) + fmt.Println() + if !loginNoOpen { + _ = openBrowser(authorizeURL) + } + fmt.Printf("Waiting for the browser login (redirect captured on %s)...\n", loginRedirectURI) + + code, err := capture.Wait(5 * time.Minute) + if err != nil { + return err + } + + tok, err := auth.ExchangeCode(domain, clientID, code, verifier, loginRedirectURI) + if err != nil { + return err + } + return saveAuth0Token(tok, useIDToken, domain, clientID) +} + +func saveAuth0Token(tok *auth.Token, useIDToken bool, domain, clientID string) error { + if err := config.SaveToken(&config.TokenInfo{ + AccessToken: tok.AccessToken, + IDToken: tok.IDToken, + RefreshToken: tok.RefreshToken, + ExpiresAt: time.Now().Add(time.Duration(tok.ExpiresIn) * time.Second).Unix(), + UseIDToken: useIDToken, + Auth0Domain: domain, + ClientID: clientID, + }); err != nil { + return fmt.Errorf("store token: %w", err) + } + color.Green("Logged in. Token stored in %s", config.TokenPath()) + return nil +} + var logoutCmd = &cobra.Command{ Use: "logout", Short: "Remove the stored auth token", RunE: func(cmd *cobra.Command, args []string) error { - cfg := config.Load() - if cfg.APIKey == "" { - color.Yellow("Not currently logged in.") - return nil + cleared := false + if t, _ := config.LoadToken(); t != nil { + if err := config.ClearToken(); err != nil { + return fmt.Errorf("clear token: %w", err) + } + cleared = true } - cfg.APIKey = "" - if err := config.Save(cfg); err != nil { - return fmt.Errorf("save config: %w", err) + // Also clear any legacy API key stored in config.json. + if c := config.Load(); c.APIKey != "" { + c.APIKey = "" + if err := config.Save(c); err != nil { + return fmt.Errorf("save config: %w", err) + } + cleared = true + } + if cleared { + color.Green("Logged out.") + } else { + color.Yellow("Not currently logged in.") } - color.Green("Logged out.") return nil }, } -// doLogin calls the server auth endpoint and persists the returned token. -// Extracted so tests can call it directly without terminal I/O. -func doLogin(cfg *config.Config, username, password string) error { - c := client.New(cfg) - resp, err := c.Login(username, password) - if err != nil { - return fmt.Errorf("login failed: %w", err) - } - if resp.Token == "" { - return fmt.Errorf("server returned empty token") +// openBrowser best-effort opens a URL in the default browser. +func openBrowser(url string) error { + var name string + var args []string + switch runtime.GOOS { + case "darwin": + name, args = "open", []string{url} + case "windows": + name, args = "rundll32", []string{"url.dll,FileProtocolHandler", url} + default: + name, args = "xdg-open", []string{url} } - cfg.APIKey = resp.Token - if err := config.Save(cfg); err != nil { - return fmt.Errorf("save config: %w", err) - } - return nil + return exec.Command(name, args...).Start() } func init() { + loginCmd.Flags().StringVar(&loginKeyID, "key-id", "", "orkes access key id (service-account auth; env AGENTSPAN_AUTH_KEY)") + loginCmd.Flags().StringVar(&loginKeySecret, "key-secret", "", "orkes access key secret (service-account auth; env AGENTSPAN_AUTH_SECRET)") + loginCmd.Flags().StringVar(&loginDomain, "auth0-domain", "", "Auth0 domain (default: discovered from server /context.js)") + loginCmd.Flags().StringVar(&loginClientID, "auth0-client-id", "", "Auth0 client id (default: discovered from server /context.js)") + loginCmd.Flags().StringVar(&loginScope, "scope", auth.DefaultScope, "OAuth scope to request") + loginCmd.Flags().BoolVar(&loginNoOpen, "no-open", false, "Do not auto-open the browser") + loginCmd.Flags().BoolVar(&loginDevice, "device", false, "Use the OAuth Device Authorization flow instead of the browser login (headless machines)") + loginCmd.Flags().StringVar(&loginRedirectURI, "redirect-uri", "http://localhost:5001", "Loopback redirect URI for the browser login; must exactly match an Allowed Callback URL of the Auth0 app (default: the local orkes UI origin)") rootCmd.AddCommand(loginCmd) rootCmd.AddCommand(logoutCmd) } diff --git a/cli/cmd/login_test.go b/cli/cmd/login_test.go index 61d6d8e19..69ee03bfa 100644 --- a/cli/cmd/login_test.go +++ b/cli/cmd/login_test.go @@ -1,107 +1,104 @@ package cmd import ( - "encoding/json" - "net/http" - "net/http/httptest" + "strings" "testing" + "time" "github.com/agentspan-ai/agentspan/cli/config" ) -func TestLogoutClearsAPIKey(t *testing.T) { - newTempHome(t) - - cfg := config.DefaultConfig() - cfg.APIKey = "existing-token" - if err := config.Save(cfg); err != nil { - t.Fatalf("save: %v", err) +func TestPromptServerURL(t *testing.T) { + // Enter accepts the current default. + if got := promptServerURL(strings.NewReader("\n"), "http://localhost:6767"); got != "" { + t.Errorf("enter should keep current, got %q", got) } - - cfg.APIKey = "" - if err := config.Save(cfg); err != nil { - t.Fatalf("save cleared: %v", err) + // Full URL passes through (trailing slash trimmed). + if got := promptServerURL(strings.NewReader("https://my.orkes.io/\n"), "x"); got != "https://my.orkes.io" { + t.Errorf("got %q", got) } - - loaded := config.Load() - if loaded.APIKey != "" { - t.Errorf("APIKey after logout = %q, want empty", loaded.APIKey) + // Missing scheme defaults to http:// (local instances). + if got := promptServerURL(strings.NewReader("localhost:8080\n"), "x"); got != "http://localhost:8080" { + t.Errorf("got %q", got) } } -func TestLoginStoresToken(t *testing.T) { +func TestGetConfigPersistsExplicitServer(t *testing.T) { newTempHome(t) + old := serverURL + defer func() { serverURL = old }() - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost || r.URL.Path != "/api/auth/login" { - http.NotFound(w, r) - return - } - var body map[string]string - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - http.Error(w, "bad body", http.StatusBadRequest) - return - } - if body["username"] != "alice" || body["password"] != "secret" { - http.Error(w, "unauthorized", http.StatusUnauthorized) - return - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"token": "jwt-abc123"}) - })) - defer srv.Close() + serverURL = "http://localhost:8080" + cfg := getConfig() + if cfg.ServerURL != "http://localhost:8080" { + t.Fatalf("effective server = %q", cfg.ServerURL) + } + // The explicit --server must now be the persisted default. + if got := config.FileServerURL(); got != "http://localhost:8080" { + t.Errorf("persisted default = %q, want http://localhost:8080", got) + } + // And a flag-less invocation picks it up. + serverURL = "" + if cfg2 := getConfig(); cfg2.ServerURL != "http://localhost:8080" { + t.Errorf("flag-less server = %q, want persisted default", cfg2.ServerURL) + } +} - cfg := config.DefaultConfig() - cfg.ServerURL = srv.URL - if err := config.Save(cfg); err != nil { +func TestSaveDefaultServerPreservesAPIKey(t *testing.T) { + newTempHome(t) + if err := config.Save(&config.Config{ServerURL: "http://a", APIKey: "keep-me"}); err != nil { t.Fatalf("save: %v", err) } - - if err := doLogin(cfg, "alice", "secret"); err != nil { - t.Fatalf("doLogin: %v", err) + if err := config.SaveDefaultServer("http://b"); err != nil { + t.Fatalf("SaveDefaultServer: %v", err) } - - loaded := config.Load() - if loaded.APIKey != "jwt-abc123" { - t.Errorf("APIKey = %q, want jwt-abc123", loaded.APIKey) + cfg := config.Load() + if cfg.ServerURL != "http://b" || cfg.APIKey != "keep-me" { + t.Errorf("got %+v, want server http://b with api key preserved", cfg) } } -func TestLoginServerError(t *testing.T) { +func TestLogoutClearsToken(t *testing.T) { newTempHome(t) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "unauthorized", http.StatusUnauthorized) - })) - defer srv.Close() + if err := config.SaveToken(&config.TokenInfo{ + AccessToken: "jwt-abc", + ExpiresAt: time.Now().Add(time.Hour).Unix(), + }); err != nil { + t.Fatalf("save token: %v", err) + } - cfg := config.DefaultConfig() - cfg.ServerURL = srv.URL - if err := config.Save(cfg); err != nil { - t.Fatalf("save: %v", err) + if err := config.ClearToken(); err != nil { + t.Fatalf("clear token: %v", err) } - if err := doLogin(cfg, "bad", "creds"); err == nil { - t.Fatal("expected error from doLogin on 401, got nil") + tok, err := config.LoadToken() + if err != nil { + t.Fatalf("load token: %v", err) + } + if tok != nil { + t.Errorf("token after logout = %+v, want nil", tok) } } -func TestLoginEmptyTokenError(t *testing.T) { - newTempHome(t) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"token": ""}) - })) - defer srv.Close() - - cfg := config.DefaultConfig() - cfg.ServerURL = srv.URL - if err := config.Save(cfg); err != nil { - t.Fatalf("save: %v", err) +func TestTokenHeaderSelection(t *testing.T) { + access := &config.TokenInfo{AccessToken: "acc", IDToken: "id", UseIDToken: false} + if got := access.Header(); got != "acc" { + t.Errorf("default header = %q, want access token", got) + } + idtok := &config.TokenInfo{AccessToken: "acc", IDToken: "id", UseIDToken: true} + if got := idtok.Header(); got != "id" { + t.Errorf("useIdToken header = %q, want id token", got) } +} - if err := doLogin(cfg, "user", "pass"); err == nil { - t.Fatal("expected error for empty token, got nil") +func TestTokenExpired(t *testing.T) { + expired := &config.TokenInfo{ExpiresAt: time.Now().Add(-time.Minute).Unix()} + if !expired.Expired() { + t.Error("expected expired token to report Expired()=true") + } + fresh := &config.TokenInfo{ExpiresAt: time.Now().Add(time.Hour).Unix()} + if fresh.Expired() { + t.Error("expected fresh token to report Expired()=false") } } diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 9c1b7a58e..d61283e4e 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -58,7 +58,7 @@ func Execute() { } func init() { - rootCmd.PersistentFlags().StringVar(&serverURL, "server", "", "Runtime server URL (default: http://localhost:6767)") + rootCmd.PersistentFlags().StringVar(&serverURL, "server", "", "Runtime server URL; once passed it is saved as the default for subsequent commands (initial default: http://localhost:6767)") rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(tuiCmd) } diff --git a/cli/config/config.go b/cli/config/config.go index ceb327e1e..96ccfa843 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -83,3 +83,29 @@ func Save(cfg *Config) error { } return os.WriteFile(configPath(), data, 0o600) } + +// FileServerURL returns the server URL stored in config.json (no env/default merging). +// Empty when no config file exists or it has no server_url. +func FileServerURL() string { + data, err := os.ReadFile(configPath()) + if err != nil { + return "" + } + var fileCfg Config + if json.Unmarshal(data, &fileCfg) != nil { + return "" + } + return fileCfg.ServerURL +} + +// SaveDefaultServer persists serverURL as the default in config.json, preserving any +// other stored fields (e.g. a legacy api_key). Used so an explicitly passed --server +// (or the URL confirmed at login) becomes the default for subsequent commands. +func SaveDefaultServer(serverURL string) error { + fileCfg := &Config{} + if data, err := os.ReadFile(configPath()); err == nil { + _ = json.Unmarshal(data, fileCfg) + } + fileCfg.ServerURL = serverURL + return Save(fileCfg) +} diff --git a/cli/config/token.go b/cli/config/token.go new file mode 100644 index 000000000..6640cdbc3 --- /dev/null +++ b/cli/config/token.go @@ -0,0 +1,85 @@ +// Copyright (c) 2025 AgentSpan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package config + +import ( + "encoding/json" + "os" + "path/filepath" + "time" +) + +// TokenInfo is the auth token persisted at ~/.agentspan/token. It holds the Auth0 +// JWT sent to orkes as the X-Authorization header, plus the refresh token and the +// issuer details needed to refresh it without re-login. +type TokenInfo struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + ExpiresAt int64 `json:"expires_at"` // unix seconds + UseIDToken bool `json:"use_id_token"` + Auth0Domain string `json:"auth0_domain,omitempty"` + ClientID string `json:"client_id,omitempty"` + + // orkes internal-key (service-account) grant: re-mint via POST {ServerURL}/api/token. + KeyID string `json:"key_id,omitempty"` + KeySecret string `json:"key_secret,omitempty"` + ServerURL string `json:"server_url,omitempty"` +} + +// TokenPath is the dedicated token file: ~/.agentspan/token. +func TokenPath() string { + return filepath.Join(ConfigDir(), "token") +} + +// Header returns the JWT to send as X-Authorization: the ID token when the +// deployment is configured for it, otherwise the access token (UI default). +func (t *TokenInfo) Header() string { + if t.UseIDToken && t.IDToken != "" { + return t.IDToken + } + return t.AccessToken +} + +// Expired reports whether the token is at/near expiry (30s clock-skew margin). +func (t *TokenInfo) Expired() bool { + return t.ExpiresAt > 0 && time.Now().Unix() >= t.ExpiresAt-30 +} + +// SaveToken writes the token file with 0600 perms. +func SaveToken(t *TokenInfo) error { + if err := os.MkdirAll(ConfigDir(), 0o700); err != nil { + return err + } + data, err := json.MarshalIndent(t, "", " ") + if err != nil { + return err + } + return os.WriteFile(TokenPath(), data, 0o600) +} + +// LoadToken reads the token file. Returns (nil, nil) when no token is stored. +func LoadToken() (*TokenInfo, error) { + data, err := os.ReadFile(TokenPath()) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + var t TokenInfo + if err := json.Unmarshal(data, &t); err != nil { + return nil, err + } + return &t, nil +} + +// ClearToken removes the token file (logout). No-op if absent. +func ClearToken() error { + err := os.Remove(TokenPath()) + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} diff --git a/sdk/python/src/agentspan/agents/_internal/token_utils.py b/sdk/python/src/agentspan/agents/_internal/token_utils.py new file mode 100644 index 000000000..7597c2aec --- /dev/null +++ b/sdk/python/src/agentspan/agents/_internal/token_utils.py @@ -0,0 +1,100 @@ +# Copyright (c) 2025 Agentspan +# Licensed under the MIT License. See LICENSE file in the project root for details. + +"""Auth token helpers shared by the sync/async agent API clients and framework adapters. + +Secured Conductor hosts (e.g. orkes) authenticate API calls with a JWT in the +``X-Authorization`` header, minted from an application access key via +``POST {server}/token``. These helpers centralize that mint (with an expiry-aware +process-wide cache) so every HTTP path — agent API, SSE streaming, framework event +pushes — sends the same correct header. Anonymous servers ignore the header. +""" + +from __future__ import annotations + +import base64 +import json +import logging +import threading +from typing import Dict, Optional, Tuple + +logger = logging.getLogger("agentspan.agents.token_utils") + + +def decode_jwt_exp(token: str) -> float: + """Best-effort decode of a JWT's ``exp`` claim (unix seconds). + + Returns 0.0 for opaque tokens, malformed JWTs, or tokens without ``exp`` — + callers treat 0 as "expiry unknown, use until rejected". + """ + try: + parts = token.split(".") + if len(parts) < 2: + return 0.0 + seg = parts[1] + "=" * (-len(parts[1]) % 4) + payload = json.loads(base64.urlsafe_b64decode(seg)) + return float(payload.get("exp", 0) or 0) + except Exception: + return 0.0 + + +# Process-wide mint cache: (server_url, auth_key) -> (token, exp). Framework event +# pushes run on thread pools, so guard with a lock. +_TOKEN_CACHE: Dict[Tuple[str, str], Tuple[str, float]] = {} +_TOKEN_LOCK = threading.Lock() + + +def resolve_agent_api_token( + server_url: str, + api_key: Optional[str] = None, + auth_key: Optional[str] = None, + auth_secret: Optional[str] = None, +) -> Optional[str]: + """Resolve the JWT for agent API calls. + + An explicit ``api_key`` is already a token and returned as-is. Otherwise a JWT is + minted from ``auth_key``/``auth_secret`` via ``POST {server_url}/token`` and cached + until ~expiry. Returns None when no credentials are configured or the mint fails + (anonymous / security-disabled servers). + """ + if api_key: + return api_key + if not auth_key or not auth_secret: + return None + + import time + + cache_key = (server_url.rstrip("/"), auth_key) + with _TOKEN_LOCK: + cached = _TOKEN_CACHE.get(cache_key) + if cached: + token, exp = cached + if exp == 0.0 or time.time() < exp - 30: + return token + + import requests + + url = server_url.rstrip("/") + "/token" + try: + resp = requests.post(url, json={"keyId": auth_key, "keySecret": auth_secret}, timeout=30) + resp.raise_for_status() + token = resp.json().get("token") + except Exception as e: # pragma: no cover - network/credential failures + logger.warning("Failed to mint agent API token: %s", e) + return None + if not token: + return None + with _TOKEN_LOCK: + _TOKEN_CACHE[cache_key] = (token, decode_jwt_exp(token)) + return token + + +def agent_api_auth_headers( + server_url: str, + api_key: Optional[str] = None, + auth_key: Optional[str] = None, + auth_secret: Optional[str] = None, +) -> Dict[str, str]: + """``X-Authorization`` header dict for agent API calls ({} when anonymous).""" + token = resolve_agent_api_token(server_url, api_key, auth_key, auth_secret) + return {"X-Authorization": token} if token else {} diff --git a/sdk/python/src/agentspan/agents/frameworks/claude_agent_sdk.py b/sdk/python/src/agentspan/agents/frameworks/claude_agent_sdk.py index 0b9ec3e5a..5fe03ade2 100644 --- a/sdk/python/src/agentspan/agents/frameworks/claude_agent_sdk.py +++ b/sdk/python/src/agentspan/agents/frameworks/claude_agent_sdk.py @@ -21,6 +21,7 @@ from typing import Any, Dict, List, Optional, Tuple from agentspan.agents.frameworks.serializer import WorkerInfo +from agentspan.agents._internal.token_utils import agent_api_auth_headers logger = logging.getLogger("agentspan.agents.frameworks.claude_agent_sdk") @@ -830,11 +831,7 @@ def _do_push(): import requests url = f"{server_url}/agent/events/{execution_id}" - headers: Dict[str, str] = {} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers = agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) requests.post(url, json=event, headers=headers, timeout=5) except Exception as exc: logger.debug("Event push failed (execution_id=%s): %s", execution_id, exc) @@ -871,10 +868,9 @@ def _do_update(): url = f"{server_url}/tasks" headers: Dict[str, str] = {"Content-Type": "application/json"} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers.update( + agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) + ) body = { "taskId": task_id, "workflowInstanceId": execution_id, @@ -917,10 +913,9 @@ def _create_tracking_workflow( url = f"{server_url}/agent/execution" headers: Dict[str, str] = {"Content-Type": "application/json"} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers.update( + agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) + ) body: Dict[str, Any] = {"workflowName": workflow_name, "input": input_data} if parent_workflow_id: body["parentWorkflowId"] = parent_workflow_id @@ -959,10 +954,9 @@ def _inject_tool_task( url = f"{server_url}/agent/{execution_id}/tasks" headers: Dict[str, str] = {"Content-Type": "application/json"} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers.update( + agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) + ) body: Dict[str, Any] = { "taskDefName": tool_name, "referenceTaskName": ref_name, @@ -1003,10 +997,9 @@ def _do_complete(): url = f"{server_url}/agent/tasks/{execution_id}/{ref_name}/{status}" headers: Dict[str, str] = {"Content-Type": "application/json"} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers.update( + agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) + ) requests.post(url, json=output_data, headers=headers, timeout=5) except Exception as exc: logger.debug( @@ -1037,10 +1030,9 @@ def _do_complete(): url = f"{server_url}/agent/execution/{workflow_execution_id}/complete" headers: Dict[str, str] = {"Content-Type": "application/json"} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers.update( + agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) + ) requests.post(url, json=output_data or {}, headers=headers, timeout=5) except Exception as exc: logger.debug( diff --git a/sdk/python/src/agentspan/agents/frameworks/langchain.py b/sdk/python/src/agentspan/agents/frameworks/langchain.py index 4e1cf8085..c1cb49395 100644 --- a/sdk/python/src/agentspan/agents/frameworks/langchain.py +++ b/sdk/python/src/agentspan/agents/frameworks/langchain.py @@ -13,6 +13,7 @@ from langchain_core.callbacks import BaseCallbackHandler from agentspan.agents.frameworks.serializer import WorkerInfo +from agentspan.agents._internal.token_utils import agent_api_auth_headers logger = logging.getLogger("agentspan.agents.frameworks.langchain") @@ -240,11 +241,7 @@ def _do_push(): import requests url = f"{server_url}/agent/events/{execution_id}" - headers = {} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers = agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) requests.post(url, json=event, headers=headers, timeout=5) except Exception as exc: logger.debug("Event push failed (execution_id=%s): %s", execution_id, exc) diff --git a/sdk/python/src/agentspan/agents/frameworks/langgraph.py b/sdk/python/src/agentspan/agents/frameworks/langgraph.py index 7213f0e2e..73ce72b41 100644 --- a/sdk/python/src/agentspan/agents/frameworks/langgraph.py +++ b/sdk/python/src/agentspan/agents/frameworks/langgraph.py @@ -26,6 +26,7 @@ from typing import Any, Dict, List, Optional, Tuple from agentspan.agents.frameworks.serializer import WorkerInfo +from agentspan.agents._internal.token_utils import agent_api_auth_headers logger = logging.getLogger("agentspan.agents.frameworks.langgraph") @@ -1780,11 +1781,7 @@ def _do_push(): import requests url = f"{server_url}/agent/events/{execution_id}" - headers = {} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers = agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) requests.post(url, json=event, headers=headers, timeout=5) except Exception as exc: logger.debug("Event push failed (execution_id=%s): %s", execution_id, exc) diff --git a/sdk/python/src/agentspan/agents/runtime/http_client.py b/sdk/python/src/agentspan/agents/runtime/http_client.py index 571a44fe7..0d2b5843c 100644 --- a/sdk/python/src/agentspan/agents/runtime/http_client.py +++ b/sdk/python/src/agentspan/agents/runtime/http_client.py @@ -20,6 +20,7 @@ import httpx +from agentspan.agents._internal.token_utils import decode_jwt_exp from agentspan.agents.exceptions import _raise_api_error logger = logging.getLogger("agentspan.agents.runtime.http_client") @@ -46,23 +47,43 @@ def __init__( self._auth_key = auth_key self._auth_secret = auth_secret self._client: Optional[httpx.AsyncClient] = None + self._token: str = "" + self._token_exp: float = 0.0 - def _base_headers(self) -> Dict[str, str]: - headers: Dict[str, str] = {} + async def _auth_headers(self) -> Dict[str, str]: + """``X-Authorization`` header for secured hosts (orkes); {} when anonymous. + + An explicit api_key is already a token. Otherwise a JWT is minted from + auth_key/auth_secret via ``POST {server}/token`` and cached until ~expiry. + """ if self._api_key: - headers["Authorization"] = f"Bearer {self._api_key}" - elif self._auth_key: - headers["X-Auth-Key"] = self._auth_key - if self._auth_secret: - headers["X-Auth-Secret"] = self._auth_secret - return headers + return {"X-Authorization": self._api_key} + if not self._auth_key or not self._auth_secret: + return {} + + if self._token and (self._token_exp == 0.0 or time.time() < self._token_exp - 30): + return {"X-Authorization": self._token} + + try: + client = await self._get_client() + resp = await client.post( + f"{self._server_url}/token", + json={"keyId": self._auth_key, "keySecret": self._auth_secret}, + ) + resp.raise_for_status() + token = resp.json().get("token") or "" + except Exception as e: # pragma: no cover - network/credential failures + logger.warning("Failed to mint agent API token: %s", e) + return {} + if not token: + return {} + self._token = token + self._token_exp = decode_jwt_exp(token) + return {"X-Authorization": token} async def _get_client(self) -> httpx.AsyncClient: if self._client is None or self._client.is_closed: - self._client = httpx.AsyncClient( - timeout=httpx.Timeout(30.0, connect=5.0), - headers=self._base_headers(), - ) + self._client = httpx.AsyncClient(timeout=httpx.Timeout(30.0, connect=5.0)) return self._client def _url(self, path: str) -> str: @@ -74,7 +95,7 @@ async def start_agent(self, payload: Dict[str, Any]) -> Dict[str, Any]: """POST /agent/start — start an agent execution.""" client = await self._get_client() url = self._url("/start") - resp = await client.post(url, json=payload) + resp = await client.post(url, json=payload, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -85,7 +106,7 @@ async def deploy_agent(self, payload: Dict[str, Any]) -> Dict[str, Any]: """POST /agent/deploy — deploy agent (compile + register, no execution).""" client = await self._get_client() url = self._url("/deploy") - resp = await client.post(url, json=payload) + resp = await client.post(url, json=payload, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -96,7 +117,7 @@ async def compile_agent(self, config_json: Dict[str, Any]) -> Dict[str, Any]: """POST /agent/compile — compile agent config to agent def.""" client = await self._get_client() url = self._url("/compile") - resp = await client.post(url, json=config_json) + resp = await client.post(url, json=config_json, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -107,7 +128,7 @@ async def get_status(self, execution_id: str) -> Dict[str, Any]: """GET /agent/{id}/status — fetch execution status.""" client = await self._get_client() url = self._url(f"/{execution_id}/status") - resp = await client.get(url) + resp = await client.get(url, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -118,7 +139,7 @@ async def respond(self, execution_id: str, body: Dict[str, Any]) -> None: """POST /agent/{id}/respond — complete a pending human task.""" client = await self._get_client() url = self._url(f"/{execution_id}/respond") - resp = await client.post(url, json=body) + resp = await client.post(url, json=body, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -128,7 +149,7 @@ async def stop(self, execution_id: str) -> None: """POST /agent/{id}/stop — graceful deterministic stop.""" client = await self._get_client() url = self._url(f"/{execution_id}/stop") - resp = await client.post(url) + resp = await client.post(url, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -138,7 +159,7 @@ async def signal(self, execution_id: str, message: str) -> None: """POST /agent/{id}/signal — inject persistent context.""" client = await self._get_client() url = self._url(f"/{execution_id}/signal") - resp = await client.post(url, json={"message": message}) + resp = await client.post(url, json={"message": message}, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -152,7 +173,7 @@ async def stream_sse(self, execution_id: str) -> AsyncIterator[Dict[str, Any]]: server doesn't support SSE or sends only heartbeats. """ url = f"{self._server_url}/agent/stream/{execution_id}" - headers = {**self._base_headers(), "Accept": "text/event-stream"} + headers = {**(await self._auth_headers()), "Accept": "text/event-stream"} last_event_id: Optional[str] = None first_connect = True diff --git a/sdk/python/src/agentspan/agents/runtime/runtime.py b/sdk/python/src/agentspan/agents/runtime/runtime.py index ad9255a22..377d236a3 100644 --- a/sdk/python/src/agentspan/agents/runtime/runtime.py +++ b/sdk/python/src/agentspan/agents/runtime/runtime.py @@ -148,6 +148,47 @@ async def _call_user_fn(fn, *args, **kwargs): return await asyncio.to_thread(fn, *args, **kwargs) +def _resolve_loop_iteration(iteration: object) -> int: + """Resolve the current DO_WHILE loop iteration robustly across Conductor cores. + + The compiler wires a worker's ``iteration`` input from the loop counter + reference ``${_loop.iteration}``. Conductor cores disagree on whether + that loop-output reference resolves for tasks executing *inside* the loop + body: the OSS core agentspan compiles against resolves it to the live integer, + but some embedding hosts' cores (e.g. orkes-conductor) leave it unresolved and + deliver ``None`` — which then crashes ``iteration >= max_retries`` comparisons. + + The authoritative per-iteration value is always present on the Task object + (``task.iteration``), set identically by every core (1-based inside a loop, + 0 outside). So: trust a valid integer input when present (preserves the OSS + path exactly, no regression), otherwise fall back to the live task iteration, + and finally to 0. + """ + if isinstance(iteration, bool): + return 0 + if isinstance(iteration, int): + return iteration + if isinstance(iteration, str) and iteration.strip().lstrip("-").isdigit(): + return int(iteration.strip()) + try: + from conductor.client.context.task_context import get_task_context + + live = getattr(get_task_context().task, "iteration", None) + if isinstance(live, int) and not isinstance(live, bool): + return live + except Exception: + # No task context (e.g. unit-test direct call) or core without the field. + pass + return 0 + + +def _decode_jwt_exp(token: str) -> float: + """Best-effort decode of a JWT's `exp` (unix seconds); 0 if opaque/unavailable.""" + from agentspan.agents._internal.token_utils import decode_jwt_exp + + return decode_jwt_exp(token) + + def _normalize_handoff_target(task_ref: str) -> str: """Extract the actual agent name from a Conductor sub-workflow reference. @@ -409,17 +450,33 @@ def _agent_api_url(self, path: str) -> str: base = self._config.server_url.rstrip("/") return f"{base}/agent{path}" + def _agent_api_token(self) -> "Optional[str]": + """Resolve the bearer token for agent runtime API calls (sent as X-Authorization). + + Prefers an explicit ``api_key`` (already a token). Otherwise mints and caches a JWT + from ``auth_key``/``auth_secret`` via ``POST {server}/token`` — the orkes internal-key + (service-account) auth path, the same exchange the worker client and CLI use. Returns + ``None`` when no credentials are configured (anonymous / security-disabled servers). + """ + from agentspan.agents._internal.token_utils import resolve_agent_api_token + + return resolve_agent_api_token( + self._config.server_url, + api_key=self._config.api_key, + auth_key=self._config.auth_key, + auth_secret=self._config.auth_secret, + ) + def _agent_api_headers(self, content_type: str = "application/json") -> Dict[str, str]: """Build headers for agent runtime API requests.""" headers: Dict[str, str] = {} if content_type: headers["Content-Type"] = content_type - if self._config.api_key: - headers["Authorization"] = f"Bearer {self._config.api_key}" - elif self._config.auth_key: - headers["X-Auth-Key"] = self._config.auth_key - if self._config.auth_secret: - headers["X-Auth-Secret"] = self._config.auth_secret + token = self._agent_api_token() + if token: + # orkes accepts X-Authorization (and Authorization: Bearer); the standalone + # anonymous server ignores it. X-Authorization matches the UI/CLI convention. + headers["X-Authorization"] = token return headers def _register_workflow_credentials( @@ -709,11 +766,7 @@ def _compile_via_server(self, agent: Agent) -> Any: server_url = self._config.server_url.rstrip("/") url = f"{server_url}/agent/compile" - headers = {"Content-Type": "application/json"} - if self._config.auth_key: - headers["X-Auth-Key"] = self._config.auth_key - if self._config.auth_secret: - headers["X-Auth-Secret"] = self._config.auth_secret + headers = self._agent_api_headers() payload = {"agentConfig": config_json} response = requests.post(url, json=payload, headers=headers, timeout=30) @@ -1340,6 +1393,7 @@ def make_combined(specs): async def combined_guardrail_worker( content: object = None, iteration: int = 0 ) -> object: + iteration = _resolve_loop_iteration(iteration) if content is None: content_str = "" elif isinstance(content, str): @@ -1420,6 +1474,7 @@ def _register_single_guardrail_worker(self, guardrail, domain: "Optional[str]" = g_name = guardrail.name async def guardrail_worker(content: object = None, iteration: int = 0) -> object: + iteration = _resolve_loop_iteration(iteration) if content is None: content_str = "" elif isinstance(content, str): @@ -1490,6 +1545,7 @@ def _register_stop_when_worker( task_name = f"{agent_name}_stop_when" async def stop_when_worker(result="", iteration: int = 0, messages=None) -> object: + iteration = _resolve_loop_iteration(iteration) context = {"result": result, "messages": messages or [], "iteration": iteration} try: should_stop = await _call_user_fn(stop_when_fn, context) @@ -1583,6 +1639,7 @@ def _register_termination_worker( task_name = f"{agent_name}_termination" async def termination_worker(result: str = "", iteration: int = 0) -> object: + iteration = _resolve_loop_iteration(iteration) context = {"result": result, "messages": [], "iteration": iteration} try: outcome = await _call_user_fn(termination_cond.should_terminate, context) @@ -2235,11 +2292,7 @@ def plan(self, agent: Agent) -> Any: server_url = self._config.server_url.rstrip("/") url = f"{server_url}/agent/compile" - headers = {"Content-Type": "application/json"} - if self._config.auth_key: - headers["X-Auth-Key"] = self._config.auth_key - if self._config.auth_secret: - headers["X-Auth-Secret"] = self._config.auth_secret + headers = self._agent_api_headers() response = requests.post(url, json=payload, headers=headers, timeout=30) try: @@ -3614,10 +3667,9 @@ def _stream_sse(self, execution_id: str) -> Iterator[AgentEvent]: server_url = self._config.server_url.rstrip("/") url = f"{server_url}/agent/stream/{execution_id}" headers: Dict[str, str] = {"Accept": "text/event-stream"} - if self._config.auth_key: - headers["X-Auth-Key"] = self._config.auth_key - if self._config.auth_secret: - headers["X-Auth-Secret"] = self._config.auth_secret + token = self._agent_api_token() + if token: + headers["X-Authorization"] = token last_event_id: Optional[str] = None first_connect = True diff --git a/sdk/python/tests/unit/test_sse_client.py b/sdk/python/tests/unit/test_sse_client.py index 18721ebab..fe5d63b98 100644 --- a/sdk/python/tests/unit/test_sse_client.py +++ b/sdk/python/tests/unit/test_sse_client.py @@ -93,10 +93,38 @@ def do_GET(self): except (BrokenPipeError, ConnectionResetError): pass # Client disconnected + def do_POST(self): + # Mint endpoint used by the auth-headers path: POST {server}/token + # with {"keyId", "keySecret"} -> {"token": } (orkes contract). + if self.path.endswith("/token"): + length = int(self.headers.get("Content-Length", 0)) + body = json.loads(self.rfile.read(length)) if length else {} + self.server.mint_requests = getattr(self.server, "mint_requests", []) # type: ignore[attr-defined] + self.server.mint_requests.append(body) # type: ignore[attr-defined] + data = json.dumps({"token": MOCK_MINTED_JWT}).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + return + self.send_error(404) + def log_message(self, format, *args): pass # Suppress request logs during tests +def _mock_jwt() -> str: + import base64 + + header = base64.urlsafe_b64encode(b'{"alg":"HS256"}').rstrip(b"=").decode() + payload = base64.urlsafe_b64encode(b'{"exp":4102444800}').rstrip(b"=").decode() + return f"{header}.{payload}.sig" + + +MOCK_MINTED_JWT = _mock_jwt() + + class MockSSEServer: """Lightweight SSE server for testing.""" @@ -368,7 +396,11 @@ def test_connection_refused_raises_sse_unavailable(self): class TestStreamSSEAuth: - def test_auth_headers_sent(self): + def test_auth_key_secret_mints_x_authorization(self): + """auth_key/auth_secret are exchanged for a JWT via POST /token (the + secured-host contract, e.g. orkes) and sent as X-Authorization.""" + from agentspan.agents._internal.token_utils import _TOKEN_CACHE + scenario = { "events": [ {"event": "done", "id": "1", "data": _java_event("done", output="ok")}, @@ -378,15 +410,22 @@ def test_auth_headers_sent(self): server = MockSSEServer(scenario) url = server.start() try: + _TOKEN_CACHE.clear() rt = _make_runtime(url, auth_key="my-key", auth_secret="my-secret") events = list(rt._stream_sse("test-wf")) assert len(events) == 1 + # The mint endpoint received the key/secret... + mints = getattr(server.server, "mint_requests", []) + assert mints and mints[0] == {"keyId": "my-key", "keySecret": "my-secret"} + # ...and the stream request carried the minted JWT. headers = server.received_headers - assert headers.get("X-Auth-Key") == "my-key" - assert headers.get("X-Auth-Secret") == "my-secret" + assert headers.get("X-Authorization") == MOCK_MINTED_JWT + assert "X-Auth-Key" not in headers + assert "X-Auth-Secret" not in headers finally: server.stop() + _TOKEN_CACHE.clear() def test_no_auth_headers_when_not_configured(self): scenario = { @@ -402,6 +441,7 @@ def test_no_auth_headers_when_not_configured(self): list(rt._stream_sse("test-wf")) headers = server.received_headers + assert "X-Authorization" not in headers assert "X-Auth-Key" not in headers assert "X-Auth-Secret" not in headers finally: diff --git a/sdk/python/tests/unit/test_token_utils.py b/sdk/python/tests/unit/test_token_utils.py new file mode 100644 index 000000000..1d9e95298 --- /dev/null +++ b/sdk/python/tests/unit/test_token_utils.py @@ -0,0 +1,109 @@ +# Copyright (c) 2025 Agentspan +# Licensed under the MIT License. See LICENSE file in the project root for details. + +"""Unit tests for the shared agent-API auth token helpers. + +Uses a real in-process HTTP server (no mocks, per repo test policy) to emulate the +host's POST /token mint endpoint. +""" + +import base64 +import json +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer + +import pytest + +from agentspan.agents._internal.token_utils import ( + _TOKEN_CACHE, + agent_api_auth_headers, + decode_jwt_exp, + resolve_agent_api_token, +) + + +def _jwt(exp: int) -> str: + header = base64.urlsafe_b64encode(b'{"alg":"HS256"}').rstrip(b"=").decode() + payload = base64.urlsafe_b64encode(json.dumps({"exp": exp}).encode()).rstrip(b"=").decode() + return f"{header}.{payload}.sig" + + +class _TokenHandler(BaseHTTPRequestHandler): + mint_count = 0 + token = _jwt(4102444800) # far future + + def do_POST(self): # noqa: N802 + if self.path != "/token": + self.send_response(404) + self.end_headers() + return + length = int(self.headers.get("Content-Length", 0)) + body = json.loads(self.rfile.read(length)) + if body.get("keyId") != "kid" or body.get("keySecret") != "ksec": + self.send_response(401) + self.end_headers() + return + type(self).mint_count += 1 + data = json.dumps({"token": self.token}).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def log_message(self, *args): # silence + pass + + +@pytest.fixture() +def token_server(): + _TokenHandler.mint_count = 0 + srv = HTTPServer(("127.0.0.1", 0), _TokenHandler) + t = threading.Thread(target=srv.serve_forever, daemon=True) + t.start() + url = f"http://127.0.0.1:{srv.server_address[1]}" + yield url + srv.shutdown() + _TOKEN_CACHE.clear() + + +def test_decode_jwt_exp(): + assert decode_jwt_exp(_jwt(1700000000)) == 1700000000.0 + assert decode_jwt_exp("opaque-token") == 0.0 + assert decode_jwt_exp("") == 0.0 + + +def test_api_key_passthrough(): + # An explicit api_key is already a token — no mint, returned as-is. + assert resolve_agent_api_token("http://unused", api_key="tok-123") == "tok-123" + assert agent_api_auth_headers("http://unused", api_key="tok-123") == { + "X-Authorization": "tok-123" + } + + +def test_anonymous_returns_none(): + assert resolve_agent_api_token("http://unused") is None + assert agent_api_auth_headers("http://unused") == {} + + +def test_mint_and_cache(token_server): + tok = resolve_agent_api_token(token_server, auth_key="kid", auth_secret="ksec") + assert tok == _TokenHandler.token + # Second call must hit the cache (no second mint). + tok2 = resolve_agent_api_token(token_server, auth_key="kid", auth_secret="ksec") + assert tok2 == tok + assert _TokenHandler.mint_count == 1 + assert agent_api_auth_headers(token_server, auth_key="kid", auth_secret="ksec") == { + "X-Authorization": tok + } + + +def test_expired_cache_reminted(token_server): + _TOKEN_CACHE[(token_server, "kid")] = (_jwt(100), 100.0) # long expired + tok = resolve_agent_api_token(token_server, auth_key="kid", auth_secret="ksec") + assert tok == _TokenHandler.token + assert _TokenHandler.mint_count == 1 # re-minted exactly once + + +def test_bad_credentials_none(token_server): + assert resolve_agent_api_token(token_server, auth_key="kid", auth_secret="WRONG") is None \ No newline at end of file diff --git a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java index 920bb982d..4a9bc395f 100644 --- a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java +++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java @@ -46,65 +46,11 @@ public class CredentialEnvSeeder implements ApplicationRunner { /** * Well-known provider environment variables to scan on startup. - * Sourced from the AI provider config in application.properties plus the - * additional providers listed in the UI quick-select. * - *

    AGENTSPAN_MASTER_KEY is intentionally excluded — it is the encryption - * master key and must never be stored as a credential.

    + *

    Now sourced from the shared {@link KnownProviderEnvVars#NAMES} in the library so the + * standalone server and embedding hosts (e.g. orkes-conductor) seed an identical set.

    */ - static final List KNOWN_ENV_VARS = List.of( - // Anthropic (Claude) - "ANTHROPIC_API_KEY", - "ANTHROPIC_BASE_URL", - // OpenAI (GPT-4, DALL-E, etc.) - "OPENAI_API_KEY", - "OPENAI_ORG_ID", - "OPENAI_BASE_URL", - // Google Gemini / AI Studio / Vertex AI - "GEMINI_API_KEY", - "GOOGLE_API_KEY", - "GOOGLE_CLOUD_PROJECT", - "GOOGLE_CLOUD_LOCATION", - // Azure OpenAI - "AZURE_OPENAI_API_KEY", - "AZURE_OPENAI_ENDPOINT", - "AZURE_OPENAI_BASE_URL", - "AZURE_OPENAI_DEPLOYMENT", - // Mistral AI - "MISTRAL_API_KEY", - "MISTRAL_BASE_URL", - // Cohere - "COHERE_API_KEY", - "COHERE_BASE_URL", - // xAI / Grok - "XAI_API_KEY", - "GROK_BASE_URL", - // Groq - "GROQ_API_KEY", - // Perplexity - "PERPLEXITY_API_KEY", - "PERPLEXITY_BASE_URL", - // HuggingFace - "HUGGINGFACE_API_KEY", - "HUGGINGFACE_API_TOKEN", - // Stability AI - "STABILITY_API_KEY", - // DeepSeek - "DEEPSEEK_API_KEY", - // Together AI - "TOGETHER_API_KEY", - // Replicate - "REPLICATE_API_TOKEN", - // GitHub CLI / API - "GH_TOKEN", - "GITHUB_TOKEN", - // AWS Bedrock - "AWS_ACCESS_KEY_ID", - "AWS_SECRET_ACCESS_KEY", - "AWS_REGION", - "BEDROCK_API_KEY", - // Ollama (local inference) - "OLLAMA_HOST"); + static final List KNOWN_ENV_VARS = KnownProviderEnvVars.NAMES; private final CredentialStoreProvider storeProvider; private final Function envLookup; diff --git a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java new file mode 100644 index 000000000..6d3077a46 --- /dev/null +++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.service.skill; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import dev.agentspan.runtime.model.skill.SkillDetail; +import dev.agentspan.runtime.spi.SkillMetadataDAO; + +/** + * Default {@link SkillMetadataDAO} — stores skill metadata as {@code metadata.json} files on the + * local filesystem under {@code /owners////}, with a per-skill + * {@code latest} pointer file. This is the standalone-server default; it preserves the exact + * on-disk layout used before the SPI extraction. Embedding hosts (e.g. orkes-conductor) supply a + * durable/HA implementation instead (this class ships only in {@code conductor-agentspan-server}, + * so it is never on an embedding host's classpath). + */ +@Component +public class FileSystemSkillMetadataDAO implements SkillMetadataDAO { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final Path storageRoot; + + public FileSystemSkillMetadataDAO( + @Value("${agentspan.skills.storage.directory:${java.io.tmpdir}/agentspan/skills}") String storageDir) { + this.storageRoot = Path.of(storageDir).toAbsolutePath().normalize(); + } + + @Override + public void save(SkillDetail detail, boolean makeLatest) { + Path metadataPath = metadataPath(detail.getOwnerId(), detail.getName(), detail.getVersion()); + try { + Files.createDirectories(metadataPath.getParent()); + writeDetail(metadataPath, detail); + if (makeLatest) { + Files.writeString( + latestPath(detail.getOwnerId(), detail.getName()), + detail.getVersion(), + StandardCharsets.UTF_8); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to write skill metadata: " + e.getMessage(), e); + } + } + + @Override + public Optional find(String ownerId, String name, String version) { + Path metadataPath = metadataPath(ownerId, name, version); + if (!Files.exists(metadataPath)) { + return Optional.empty(); + } + return Optional.of(readDetail(metadataPath)); + } + + @Override + public Optional latestVersion(String ownerId, String name) { + Path latest = latestPath(ownerId, name); + if (!Files.exists(latest)) { + return Optional.empty(); + } + try { + return Optional.of(Files.readString(latest, StandardCharsets.UTF_8).trim()); + } catch (IOException e) { + throw new IllegalStateException("Failed to read latest skill version: " + e.getMessage(), e); + } + } + + @Override + public List listVersions(String ownerId, String name) { + Path skillRoot = skillRoot(ownerId, name); + List details = new ArrayList<>(); + if (!Files.isDirectory(skillRoot)) { + return details; + } + try (var versions = Files.list(skillRoot)) { + for (Path versionPath : versions.filter(Files::isDirectory).toList()) { + Path metadata = versionPath.resolve("metadata.json"); + if (Files.exists(metadata)) { + details.add(readDetail(metadata)); + } + } + } catch (IOException e) { + throw new IllegalStateException("Failed to list skill versions: " + e.getMessage(), e); + } + return details; + } + + @Override + public List list(String ownerId, boolean allVersions) { + Path ownerRoot = ownerRoot(ownerId); + List details = new ArrayList<>(); + if (!Files.isDirectory(ownerRoot)) { + return details; + } + try (var skillDirs = Files.list(ownerRoot)) { + for (Path skillDir : skillDirs.filter(Files::isDirectory).toList()) { + if (allVersions) { + try (var versions = Files.list(skillDir)) { + for (Path versionPath : versions.filter(Files::isDirectory).toList()) { + Path metadata = versionPath.resolve("metadata.json"); + if (Files.exists(metadata)) { + details.add(readDetail(metadata)); + } + } + } + } else { + Path latest = skillDir.resolve("latest"); + if (Files.exists(latest)) { + String version = Files.readString(latest, StandardCharsets.UTF_8).trim(); + Path metadata = skillDir.resolve(encoded(version)).resolve("metadata.json"); + if (Files.exists(metadata)) { + details.add(readDetail(metadata)); + } + } + } + } + } catch (IOException e) { + throw new IllegalStateException("Failed to list skills: " + e.getMessage(), e); + } + return details; + } + + @Override + public void delete(String ownerId, String name, String version) { + Path dir = versionDir(ownerId, name, version); + if (!Files.exists(dir)) { + return; + } + try (var paths = Files.walk(dir)) { + for (Path p : paths.sorted(Comparator.reverseOrder()).toList()) { + Files.deleteIfExists(p); + } + Path latest = latestPath(ownerId, name); + if (Files.exists(latest) + && version.equals(Files.readString(latest, StandardCharsets.UTF_8).trim())) { + updateLatestAfterDelete(ownerId, name); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to delete skill metadata: " + e.getMessage(), e); + } + } + + private void updateLatestAfterDelete(String ownerId, String name) throws IOException { + Path skillRoot = skillRoot(ownerId, name); + if (!Files.isDirectory(skillRoot)) { + Files.deleteIfExists(latestPath(ownerId, name)); + return; + } + List remaining = listVersions(ownerId, name); + if (remaining.isEmpty()) { + Files.deleteIfExists(latestPath(ownerId, name)); + try (var children = Files.list(skillRoot)) { + if (children.findAny().isEmpty()) { + Files.deleteIfExists(skillRoot); + } + } + return; + } + remaining.sort(Comparator.comparing(SkillDetail::getCreatedAt, Comparator.nullsFirst(Long::compareTo)) + .thenComparing(SkillDetail::getVersion)); + Files.writeString( + latestPath(ownerId, name), remaining.get(remaining.size() - 1).getVersion(), StandardCharsets.UTF_8); + } + + private SkillDetail readDetail(Path metadataPath) { + try { + return MAPPER.readValue(metadataPath.toFile(), SkillDetail.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to read skill metadata: " + e.getMessage(), e); + } + } + + private void writeDetail(Path metadataPath, SkillDetail detail) { + try { + MAPPER.writerWithDefaultPrettyPrinter().writeValue(metadataPath.toFile(), detail); + } catch (IOException e) { + throw new IllegalStateException("Failed to write skill metadata: " + e.getMessage(), e); + } + } + + private Path ownerRoot(String ownerId) { + return storageRoot.resolve("owners").resolve(encoded(ownerId)); + } + + private Path skillRoot(String ownerId, String name) { + return ownerRoot(ownerId).resolve(encoded(name)); + } + + private Path versionDir(String ownerId, String name, String version) { + return skillRoot(ownerId, name).resolve(encoded(version)); + } + + private Path metadataPath(String ownerId, String name, String version) { + return versionDir(ownerId, name, version).resolve("metadata.json"); + } + + private Path latestPath(String ownerId, String name) { + return skillRoot(ownerId, name).resolve("latest"); + } + + private String encoded(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } +} diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/service/SkillRegistryServiceTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/service/SkillRegistryServiceTest.java index 5816980ad..243042b1a 100644 --- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/service/SkillRegistryServiceTest.java +++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/service/SkillRegistryServiceTest.java @@ -24,6 +24,7 @@ import dev.agentspan.runtime.context.RequestContext; import dev.agentspan.runtime.context.RequestContextHolder; import dev.agentspan.runtime.model.skill.SkillDetail; +import dev.agentspan.runtime.service.skill.FileSystemSkillMetadataDAO; import dev.agentspan.runtime.service.skill.FileSystemSkillPackageStore; class SkillRegistryServiceTest { @@ -39,12 +40,12 @@ void clearRequestContext() { @Test void registeredSkillsAreVisibleOnlyToOwner() throws Exception { SkillRegistryService service = new SkillRegistryService( - tempDir.toString(), 1024 * 1024, 1024 * 1024, 10 * 1024 * 1024, 100, - new FileSystemSkillPackageStore(tempDir.resolve("packages").toString())); + new FileSystemSkillPackageStore(tempDir.resolve("packages").toString()), + new FileSystemSkillMetadataDAO(tempDir.toString())); String skillName = "owned-skill"; String manifest = "{\"name\":\"" + skillName + "\"}"; @@ -67,12 +68,12 @@ void registeredSkillsAreVisibleOnlyToOwner() throws Exception { @Test void deletingLatestVersionPromotesPreviousVersion() throws Exception { SkillRegistryService service = new SkillRegistryService( - tempDir.toString(), 1024 * 1024, 1024 * 1024, 10 * 1024 * 1024, 100, - new FileSystemSkillPackageStore(tempDir.resolve("packages").toString())); + new FileSystemSkillPackageStore(tempDir.resolve("packages").toString()), + new FileSystemSkillMetadataDAO(tempDir.toString())); String skillName = "versioned-skill"; asUser("user-a"); @@ -90,12 +91,12 @@ void deletingLatestVersionPromotesPreviousVersion() throws Exception { @Test void sameSkillNameUsesPerOwnerLatestAndPackageStorage() throws Exception { SkillRegistryService service = new SkillRegistryService( - tempDir.toString(), 1024 * 1024, 1024 * 1024, 10 * 1024 * 1024, 100, - new FileSystemSkillPackageStore(tempDir.resolve("packages").toString())); + new FileSystemSkillPackageStore(tempDir.resolve("packages").toString()), + new FileSystemSkillMetadataDAO(tempDir.toString())); String skillName = "shared-name-skill"; asUser("user-a"); @@ -131,12 +132,12 @@ void sameSkillNameUsesPerOwnerLatestAndPackageStorage() throws Exception { @SuppressWarnings("unchecked") void skillRefRawConfigIncludesParamsAndRegisteredCrossSkills() throws Exception { SkillRegistryService service = new SkillRegistryService( - tempDir.toString(), 1024 * 1024, 1024 * 1024, 10 * 1024 * 1024, 100, - new FileSystemSkillPackageStore(tempDir.resolve("packages").toString())); + new FileSystemSkillPackageStore(tempDir.resolve("packages").toString()), + new FileSystemSkillMetadataDAO(tempDir.toString())); asUser("user-a"); service.register( @@ -175,12 +176,12 @@ void skillRefRawConfigIncludesParamsAndRegisteredCrossSkills() throws Exception @SuppressWarnings("unchecked") void registeredCrossSkillRefsArePinnedAtRegistrationTime() throws Exception { SkillRegistryService service = new SkillRegistryService( - tempDir.toString(), 1024 * 1024, 1024 * 1024, 10 * 1024 * 1024, 100, - new FileSystemSkillPackageStore(tempDir.resolve("packages").toString())); + new FileSystemSkillPackageStore(tempDir.resolve("packages").toString()), + new FileSystemSkillMetadataDAO(tempDir.toString())); asUser("user-a"); service.register( diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java index 5b6b93c39..4d3c4f5fc 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java @@ -22,6 +22,7 @@ import dev.agentspan.runtime.model.*; import dev.agentspan.runtime.service.PlanAndCompileTask; +import dev.agentspan.runtime.util.EmbeddedMode; import dev.agentspan.runtime.util.JavaScriptBuilder; import dev.agentspan.runtime.util.ModelParser; import dev.agentspan.runtime.util.ModelParser.ParsedModel; @@ -2663,7 +2664,8 @@ private String emitPlannerContextBuilder(List> entries, Stri throw new IllegalArgumentException("plannerContext header '" + name + "' contains CR/LF — rejected to prevent HTTP response splitting"); } - headers.put(name, CREDENTIAL_PLACEHOLDER.matcher(value).replaceAll("#{$1}")); + String replacement = EmbeddedMode.isEmbedded() ? "\\${workflow.secrets.$1}" : "#{$1}"; + headers.put(name, CREDENTIAL_PLACEHOLDER.matcher(value).replaceAll(replacement)); } } diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java index 5a7eea1b7..10d47cd9c 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,6 +21,7 @@ import dev.agentspan.runtime.model.GuardrailConfig; import dev.agentspan.runtime.model.ToolConfig; +import dev.agentspan.runtime.util.EmbeddedMode; import dev.agentspan.runtime.util.JavaScriptBuilder; import dev.agentspan.runtime.util.ModelParser; @@ -54,15 +56,31 @@ public static class ToolCallRoutingResult { * The {@code #} prefix is invisible to Conductor's expression engine and is later * resolved by credential-aware task handlers. */ + /** Matches a {@code ${IDENTIFIER}} credential placeholder. */ + private static final Pattern CREDENTIAL_PLACEHOLDER = Pattern.compile("\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}"); + private static Map escapeCredentialPlaceholders(Map headers) { Map escaped = new LinkedHashMap<>(); for (Map.Entry e : headers.entrySet()) { String v = String.valueOf(e.getValue()); - escaped.put(String.valueOf(e.getKey()), v.replace("${", "#{")); + escaped.put(String.valueOf(e.getKey()), rewriteCredentialPlaceholders(v)); } return escaped; } + /** + * Rewrite {@code ${NAME}} credential placeholders for the runtime that will resolve them. + * When embedded in a host (e.g. orkes-conductor), emit the host's native + * {@code ${workflow.secrets.NAME}} so the host resolves it at task-input binding. Standalone: + * escape to {@code #{NAME}} for AgentSpan's credential-aware HTTP/MCP task handlers. + */ + private static String rewriteCredentialPlaceholders(String value) { + if (EmbeddedMode.isEmbedded()) { + return CREDENTIAL_PLACEHOLDER.matcher(value).replaceAll("\\${workflow.secrets.$1}"); + } + return value.replace("${", "#{"); + } + /** * Return a copy of {@code cfg} with credential placeholders in its {@code headers} * entry escaped from {@code ${NAME}} to {@code #{NAME}}. diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/config/AgentSpanAutoConfiguration.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/config/AgentSpanAutoConfiguration.java index 7bf4df50b..a5275f28e 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/config/AgentSpanAutoConfiguration.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/config/AgentSpanAutoConfiguration.java @@ -5,7 +5,11 @@ package dev.agentspan.runtime.config; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.FilterType; /** @@ -19,6 +23,17 @@ * ({@code AgentRuntime}) scans only the Conductor packages and relies on this * auto-configuration for the AgentSpan beans. * + *

    Activation contract. Merely having this library on the classpath must not change a + * host application's behavior. The configuration activates only when: + *

      + *
    • Standalone: the standalone server distribution is the application — detected by its + * entry point ({@code dev.agentspan.runtime.AgentRuntime}, shipped only in + * {@code conductor-agentspan-server}) being on the classpath; or
    • + *
    • Embedded: the host explicitly opts in with {@code agentspan.embedded=true} + * (e.g. orkes-conductor). Without the flag, the host runs as stock Conductor — no + * AgentSpan beans, controllers, or system-task overrides are registered.
    • + *
    + * *

    The scan spans both jars (library + server) because they share the * {@code dev.agentspan.runtime} namespace: the library's contracts and logic are always * present, while the server's default SPI implementations and web config are present only @@ -28,6 +43,7 @@ * this class, which are processed directly rather than via the scan. */ @AutoConfiguration +@Conditional(AgentSpanAutoConfiguration.StandaloneOrExplicitlyEmbedded.class) @ComponentScan( basePackages = "dev.agentspan.runtime", excludeFilters = { @@ -36,4 +52,22 @@ type = FilterType.REGEX, pattern = "dev\\.agentspan\\.runtime\\.config\\.AgentSpanAutoConfiguration") }) -public class AgentSpanAutoConfiguration {} +public class AgentSpanAutoConfiguration { + + /** + * Activate when running as the standalone AgentSpan server (its entry point class is on the + * classpath) OR when an embedding host explicitly sets {@code agentspan.embedded=true}. + */ + static class StandaloneOrExplicitlyEmbedded extends AnyNestedCondition { + + StandaloneOrExplicitlyEmbedded() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnClass(name = "dev.agentspan.runtime.AgentRuntime") + static class StandaloneServer {} + + @ConditionalOnProperty(name = "agentspan.embedded", havingValue = "true") + static class HostOptedIn {} + } +} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/controller/SecretController.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/controller/SecretController.java index fc22e1652..31517e4b7 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/controller/SecretController.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/controller/SecretController.java @@ -11,6 +11,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -44,6 +45,7 @@ @RestController @RequestMapping("/api/secrets") @RequiredArgsConstructor +@ConditionalOnProperty(name = "agentspan.embedded", havingValue = "false", matchIfMissing = true) public class SecretController { private static final Logger log = LoggerFactory.getLogger(SecretController.class); diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskConfig.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskConfig.java index 46ebad7c1..6edfa8bf1 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskConfig.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskConfig.java @@ -4,6 +4,7 @@ */ package dev.agentspan.runtime.credentials; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -17,8 +18,16 @@ * *

    This follows the same pattern as {@code AgentHumanTaskConfig} which * overrides the default HUMAN task.

    + * + *

    Embedded mode: disabled when {@code agentspan.embedded=true} (e.g. when the + * library is imported into a host such as orkes-conductor). The host already provides its + * own {@code HTTP} system task — overriding it here would collide on the {@code "HTTP"} bean + * name and downgrade the host's task. The host is expected to port {@code #{NAME}} secret + * resolution into its own HTTP task, guarded by the {@code __agentspan_ctx__} input. The + * standalone OSS server leaves this property unset, so the override stays active as before.

    */ @Configuration +@ConditionalOnProperty(name = "agentspan.embedded", havingValue = "false", matchIfMissing = true) public class CredentialAwareHttpTaskConfig { @Bean("HTTP") diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/KnownProviderEnvVars.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/KnownProviderEnvVars.java new file mode 100644 index 000000000..df11e3fdf --- /dev/null +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/KnownProviderEnvVars.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. + */ +package dev.agentspan.runtime.credentials; + +import java.util.List; + +/** + * Single source of truth for the well-known LLM/tool provider environment variables that AgentSpan + * auto-seeds into the credential store on startup. + * + *

    Shared so every deployment seeds the same set: the standalone server's {@code CredentialEnvSeeder} + * and any embedding host (e.g. orkes-conductor) reference this list instead of maintaining their own + * copies. Keeping one list avoids drift between standalone and embedded behavior.

    + * + *

    {@code AGENTSPAN_MASTER_KEY} is intentionally excluded — it is the encryption master key and must + * never be stored as a credential.

    + */ +public final class KnownProviderEnvVars { + + private KnownProviderEnvVars() {} + + /** Well-known provider environment variables to scan on startup. */ + public static final List NAMES = List.of( + // Anthropic (Claude) + "ANTHROPIC_API_KEY", + "ANTHROPIC_BASE_URL", + // OpenAI (GPT-4, DALL-E, etc.) + "OPENAI_API_KEY", + "OPENAI_ORG_ID", + "OPENAI_BASE_URL", + // Google Gemini / AI Studio / Vertex AI + "GEMINI_API_KEY", + "GOOGLE_API_KEY", + "GOOGLE_CLOUD_PROJECT", + "GOOGLE_CLOUD_LOCATION", + // Azure OpenAI + "AZURE_OPENAI_API_KEY", + "AZURE_OPENAI_ENDPOINT", + "AZURE_OPENAI_BASE_URL", + "AZURE_OPENAI_DEPLOYMENT", + // Mistral AI + "MISTRAL_API_KEY", + "MISTRAL_BASE_URL", + // Cohere + "COHERE_API_KEY", + "COHERE_BASE_URL", + // xAI / Grok + "XAI_API_KEY", + "GROK_BASE_URL", + // Groq + "GROQ_API_KEY", + // Perplexity + "PERPLEXITY_API_KEY", + "PERPLEXITY_BASE_URL", + // HuggingFace + "HUGGINGFACE_API_KEY", + "HUGGINGFACE_API_TOKEN", + // Stability AI + "STABILITY_API_KEY", + // DeepSeek + "DEEPSEEK_API_KEY", + // Together AI + "TOGETHER_API_KEY", + // Replicate + "REPLICATE_API_TOKEN", + // GitHub CLI / API + "GH_TOKEN", + "GITHUB_TOKEN", + // AWS Bedrock + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_REGION", + "BEDROCK_API_KEY", + // Ollama (local inference) + "OLLAMA_HOST"); +} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentHumanTaskConfig.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentHumanTaskConfig.java index 81347ea2a..8c2ec77a6 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentHumanTaskConfig.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentHumanTaskConfig.java @@ -7,6 +7,7 @@ import static com.netflix.conductor.common.metadata.tasks.TaskType.TASK_TYPE_HUMAN; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -14,8 +15,17 @@ /** * Registers {@link AgentHumanTask} as the primary HUMAN task implementation, * overriding Conductor's default {@code Human} system task. + * + *

    Embedded mode: disabled when {@code agentspan.embedded=true} (e.g. when the + * library is imported into a host such as orkes-conductor). The host provides its own + * (richer) HUMAN task — overriding it here would collide on the {@code HUMAN} bean name and + * replace the host's full HITL implementation with this SSE-only shim. The host is expected + * to emit the {@code WAITING} SSE event from its own HUMAN task, guarded by the + * {@code __agentspan_ctx__} input. The standalone OSS server leaves this property unset, so + * the override stays active as before.

    */ @Configuration +@ConditionalOnProperty(name = "agentspan.embedded", havingValue = "false", matchIfMissing = true) public class AgentHumanTaskConfig { @Bean(TASK_TYPE_HUMAN) diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java index ba432d485..fa110d6ba 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java @@ -34,10 +34,12 @@ import com.netflix.conductor.core.exception.NotFoundException; import com.netflix.conductor.core.execution.StartWorkflowInput; import com.netflix.conductor.core.execution.WorkflowExecutor; +import com.netflix.conductor.core.utils.IDGenerator; import com.netflix.conductor.dao.ExecutionDAO; import com.netflix.conductor.dao.MetadataDAO; import com.netflix.conductor.model.WorkflowModel; import com.netflix.conductor.service.ExecutionService; +import com.netflix.conductor.service.MetadataService; import com.netflix.conductor.service.WorkflowService; import dev.agentspan.runtime.compiler.AgentCompiler; @@ -74,6 +76,26 @@ public class AgentService { @Autowired(required = false) private SkillRegistryService skillRegistryService; + /** + * Conductor's configured ID generator. When embedded in a host that uses time-based IDs + * (e.g. orkes-conductor with {@code conductor.id.generator=time_based}), pre-allocated + * execution IDs must be v1 time-based UUIDs — the host derives a workflow's createTime from + * its ID and yields 0 for non-v1 (random) UUIDs. Falls back to a random UUID when unset + * (standalone tests / no Spring context). + */ + @Autowired(required = false) + private IDGenerator idGenerator; + + /** + * Stable metadata service for task-def registration. The low-level {@code MetadataDAO}'s + * {@code createTaskDef}/{@code updateTaskDef} return types differ across Conductor cores + * (orkes' vendored oss-core returns {@code void}; 3.30.2 returns {@code TaskDef}), so calling + * the DAO directly throws {@code NoSuchMethodError} when embedded. {@code MetadataService}'s + * methods return {@code void} in all cores. Optional so the test constructor still works. + */ + @Autowired(required = false) + private MetadataService metadataService; + /** Package-private constructor for testing with ExecutionTokenService */ AgentService( AgentCompiler agentCompiler, @@ -267,6 +289,18 @@ public StartResponse start(StartRequest request) { startReq.setVersion(def.getVersion()); startReq.setWorkflowDef(def); + // Attribute the execution to the calling principal (the host populates the + // RequestContext: orkes' principal filter when embedded, the standalone AuthFilter + // otherwise). Security-enabled hosts key on this standard Conductor field: orkes + // records workflow.createdBy from it, stamps _createdBy into every scheduled task, + // impersonates it during decide so sub-workflows inherit attribution, and its + // workers' poll-time secret substitution REQUIRES it (tasks of workflows without + // createdBy fail to poll). Stock Conductor simply records the value. + String principal = RequestContextHolder.get().map(ctx -> ctx.getUserId()).orElse(null); + if (principal != null) { + startReq.setCreatedBy(principal); + } + Map input = new LinkedHashMap<>(); input.put("prompt", request.getPrompt()); input.put("media", request.getMedia() != null ? request.getMedia() : List.of()); @@ -293,7 +327,10 @@ public StartResponse start(StartRequest request) { // with NPE when it tries Map.of(..., null, ...) on the not-null // execution_id column. Passing this id to setWorkflowId on the start // input below makes Conductor adopt it instead of generating one. - String preallocatedExecutionId = UUID.randomUUID().toString(); + // Use the host's configured ID generator (time-based when embedded in orkes) so the + // host can derive createTime from the ID; fall back to a random UUID outside Spring. + String preallocatedExecutionId = + idGenerator != null ? idGenerator.generate() : UUID.randomUUID().toString(); // Mint execution token and embed in workflow variables for worker credential resolution if (executionTokenService != null) { @@ -310,8 +347,7 @@ public StartResponse start(StartRequest request) { } } } - String currentUserId = - RequestContextHolder.get().map(ctx -> ctx.getUserId()).orElse(null); + String currentUserId = principal; if (currentUserId != null) { String token = executionTokenService.mint( currentUserId, preallocatedExecutionId, declaredNames, timeoutSeconds); @@ -399,7 +435,14 @@ public StartResponse start(StartRequest request) { */ @SuppressWarnings("unchecked") public List listAgents() { - List allDefs = metadataDAO.getAllWorkflowDefsLatestVersions(); + // Use the portable getAllWorkflowDefs() (present across Conductor cores, incl. orkes' + // vendored oss-core which lacks getAllWorkflowDefsLatestVersions()) and reduce to the + // latest version per name ourselves. + Map latestByName = new HashMap<>(); + for (WorkflowDef d : metadataDAO.getAllWorkflowDefs()) { + latestByName.merge(d.getName(), d, (a, b) -> a.getVersion() >= b.getVersion() ? a : b); + } + List allDefs = new ArrayList<>(latestByName.values()); List agents = new ArrayList<>(); for (WorkflowDef def : allDefs) { @@ -1334,7 +1377,11 @@ private void registerTaskDef(String taskName) { try { TaskDef existing = metadataDAO.getTaskDef(taskName); if (existing != null) { - metadataDAO.updateTaskDef(taskDef); + if (metadataService != null) { + metadataService.updateTaskDef(taskDef); + } else { + metadataDAO.updateTaskDef(taskDef); + } log.debug("Updated task definition: {}", taskName); return; } @@ -1342,7 +1389,13 @@ private void registerTaskDef(String taskName) { // Task doesn't exist, create it } - metadataDAO.createTaskDef(taskDef); + // Prefer the stable MetadataService (registerTaskDef upserts, returns void in all cores); + // fall back to the DAO only outside Spring (tests). + if (metadataService != null) { + metadataService.registerTaskDef(List.of(taskDef)); + } else { + metadataDAO.createTaskDef(taskDef); + } log.info("Registered task definition: {}", taskName); } diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/SkillRegistryService.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/SkillRegistryService.java index 64e316204..56356f0dd 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/SkillRegistryService.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/SkillRegistryService.java @@ -9,13 +9,10 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.security.MessageDigest; import java.time.Instant; import java.util.ArrayList; @@ -25,6 +22,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.TreeMap; import java.util.regex.Matcher; @@ -48,6 +46,7 @@ import dev.agentspan.runtime.model.skill.SkillFileContent; import dev.agentspan.runtime.model.skill.SkillFileEntry; import dev.agentspan.runtime.model.skill.SkillSummary; +import dev.agentspan.runtime.spi.SkillMetadataDAO; import dev.agentspan.runtime.spi.SkillPackageStore; import dev.agentspan.runtime.spi.StoredSkillPackage; @@ -88,27 +87,27 @@ public class SkillRegistryService { ".properties", ".csv"); - private final Path storageRoot; private final long maxPackageBytes; private final long maxPreviewBytes; private final long maxUncompressedBytes; private final int maxFileCount; private final SkillPackageStore packageStore; + private final SkillMetadataDAO metadataDao; @Autowired public SkillRegistryService( - @Value("${agentspan.skills.storage.directory:${java.io.tmpdir}/agentspan/skills}") String storageDir, @Value("${agentspan.skills.max-package-bytes:52428800}") long maxPackageBytes, @Value("${agentspan.skills.max-preview-bytes:1048576}") long maxPreviewBytes, @Value("${agentspan.skills.max-uncompressed-bytes:209715200}") long maxUncompressedBytes, @Value("${agentspan.skills.max-file-count:2000}") int maxFileCount, - SkillPackageStore packageStore) { - this.storageRoot = Path.of(storageDir).toAbsolutePath().normalize(); + SkillPackageStore packageStore, + SkillMetadataDAO metadataDao) { this.maxPackageBytes = maxPackageBytes; this.maxPreviewBytes = maxPreviewBytes; this.maxUncompressedBytes = maxUncompressedBytes; this.maxFileCount = maxFileCount; this.packageStore = packageStore; + this.metadataDao = metadataDao; } public synchronized SkillDetail register(String manifestJson, MultipartFile packageFile) { @@ -139,11 +138,10 @@ public synchronized SkillDetail register(String manifestJson, MultipartFile pack long now = Instant.now().toEpochMilli(); String storageName = packageStoreName(ownerId, name); - Path versionDir = versionDir(ownerId, name, version); - Path metadataPath = metadataPath(ownerId, name, version); - if (Files.exists(metadataPath)) { - SkillDetail existing = readDetail(metadataPath); + Optional existingOpt = metadataDao.find(ownerId, name, version); + if (existingOpt.isPresent()) { + SkillDetail existing = existingOpt.get(); enforceReadable(existing); if (!checksum.equals(existing.getChecksum())) { throw new IllegalArgumentException( @@ -155,14 +153,13 @@ public synchronized SkillDetail register(String manifestJson, MultipartFile pack existing.setStorageType(restored.storageType()); existing.setPackageSize(restored.size()); existing.setUpdatedAt(now); - writeDetail(metadataPath, existing); + metadataDao.save(existing, false); } return existing; } StoredSkillPackage stored = null; try { - Files.createDirectories(versionDir); stored = packageStore.store(storageName, version, checksum, bytes); SkillDetail detail = SkillDetail.builder() @@ -182,14 +179,8 @@ public synchronized SkillDetail register(String manifestJson, MultipartFile pack .metadata(parsed.metadata()) .rawConfig(rawConfig) .build(); - writeDetail(metadataPath, detail); - Files.writeString(latestPath(ownerId, name), version, StandardCharsets.UTF_8); + metadataDao.save(detail, true); return detail; - } catch (IOException e) { - if (stored != null) { - packageStore.delete(stored.handle()); - } - throw new IllegalStateException("Failed to store skill package: " + e.getMessage(), e); } catch (RuntimeException e) { if (stored != null) { packageStore.delete(stored.handle()); @@ -199,31 +190,11 @@ public synchronized SkillDetail register(String manifestJson, MultipartFile pack } public List list(boolean allVersions) { - Path ownerRoot = ownerRoot(currentUserId()); - if (!Files.isDirectory(ownerRoot)) { - return List.of(); - } List summaries = new ArrayList<>(); - try (var skillDirs = Files.list(ownerRoot)) { - for (Path skillDir : skillDirs.filter(Files::isDirectory).toList()) { - if (allVersions) { - try (var versions = Files.list(skillDir)) { - for (Path versionPath : - versions.filter(Files::isDirectory).toList()) { - addSummary(summaries, versionPath.resolve("metadata.json")); - } - } - } else { - Path latest = skillDir.resolve("latest"); - if (Files.exists(latest)) { - String version = - Files.readString(latest, StandardCharsets.UTF_8).trim(); - addSummary(summaries, skillDir.resolve(encoded(version)).resolve("metadata.json")); - } - } + for (SkillDetail detail : metadataDao.list(currentUserId(), allVersions)) { + if (isReadable(detail)) { + addSummary(summaries, detail); } - } catch (IOException e) { - throw new IllegalStateException("Failed to list skills: " + e.getMessage(), e); } summaries.sort(Comparator.comparing(SkillSummary::getName).thenComparing(SkillSummary::getVersion)); return summaries; @@ -232,11 +203,9 @@ public List list(boolean allVersions) { public SkillDetail get(String name, String version) { String ownerId = currentUserId(); String resolvedVersion = resolveVersion(ownerId, name, version); - Path metadataPath = metadataPath(ownerId, name, resolvedVersion); - if (!Files.exists(metadataPath)) { - throw new IllegalArgumentException("Skill not found: " + name + "@" + resolvedVersion); - } - SkillDetail detail = readDetail(metadataPath); + SkillDetail detail = metadataDao + .find(ownerId, name, resolvedVersion) + .orElseThrow(() -> new IllegalArgumentException("Skill not found: " + name + "@" + resolvedVersion)); enforceReadable(detail); return detail; } @@ -330,42 +299,14 @@ public Map rawConfigForDeploy( public synchronized void delete(String name, String version) { String ownerId = currentUserId(); String resolvedVersion = resolveVersion(ownerId, name, version); - Path dir = versionDir(ownerId, name, resolvedVersion); - if (!Files.exists(dir)) { - return; - } - Path metadataPath = metadataPath(ownerId, name, resolvedVersion); - SkillDetail detail = null; - if (Files.exists(metadataPath)) { - detail = readDetail(metadataPath); + metadataDao.find(ownerId, name, resolvedVersion).ifPresent(detail -> { enforceReadable(detail); - } - try (var paths = Files.walk(dir)) { - for (Path p : paths.sorted(Comparator.reverseOrder()).toList()) { - Files.deleteIfExists(p); - } - if (detail != null) { - deletePackage(detail); - } - Path latest = latestPath(ownerId, name); - if (Files.exists(latest) - && resolvedVersion.equals( - Files.readString(latest, StandardCharsets.UTF_8).trim())) { - updateLatestAfterDelete(ownerId, name); - } - } catch (IOException e) { - throw new IllegalStateException("Failed to delete skill: " + e.getMessage(), e); - } + deletePackage(detail); + }); + metadataDao.delete(ownerId, name, resolvedVersion); } - private void addSummary(List summaries, Path metadataPath) { - if (!Files.exists(metadataPath)) { - return; - } - SkillDetail detail = readDetail(metadataPath); - if (!isReadable(detail)) { - return; - } + private void addSummary(List summaries, SkillDetail detail) { Map raw = detail.getRawConfig() != null ? detail.getRawConfig() : Map.of(); summaries.add(SkillSummary.builder() .name(detail.getName()) @@ -389,34 +330,20 @@ private String resolveVersion(String ownerId, String name, String version) { version = null; } if (version != null && !version.isBlank()) { - Path direct = metadataPath(ownerId, name, version); - if (Files.exists(direct)) { + if (metadataDao.find(ownerId, name, version).isPresent()) { return version; } - Path skillRoot = skillRoot(ownerId, name); - if (Files.isDirectory(skillRoot)) { - try (var versions = Files.list(skillRoot)) { - for (Path candidate : versions.filter(Files::isDirectory).toList()) { - SkillDetail detail = readDetail(candidate.resolve("metadata.json")); - if (detail.getChecksum() != null && detail.getChecksum().startsWith(version)) { - return detail.getVersion(); - } - } - } catch (IOException e) { - throw new IllegalStateException("Failed to resolve skill version: " + e.getMessage(), e); + // Allow a checksum prefix in place of an exact version. + for (SkillDetail detail : metadataDao.listVersions(ownerId, name)) { + if (detail.getChecksum() != null && detail.getChecksum().startsWith(version)) { + return detail.getVersion(); } } return version; } - Path latest = latestPath(ownerId, name); - if (!Files.exists(latest)) { - throw new IllegalArgumentException("Skill not found: " + name); - } - try { - return Files.readString(latest, StandardCharsets.UTF_8).trim(); - } catch (IOException e) { - throw new IllegalStateException("Failed to read latest skill version: " + e.getMessage(), e); - } + return metadataDao + .latestVersion(ownerId, name) + .orElseThrow(() -> new IllegalArgumentException("Skill not found: " + name)); } private Map parseManifest(String manifestJson) { @@ -750,7 +677,9 @@ private void pinRegisteredCrossSkillRefs(String ownerId, Map raw SkillDetail refDetail; try { String refVersion = resolveVersion(ownerId, refName, null); - refDetail = readDetail(metadataPath(ownerId, refName, refVersion)); + refDetail = metadataDao + .find(ownerId, refName, refVersion) + .orElseThrow(() -> new IllegalArgumentException("Skill not found: " + refName)); } catch (IllegalArgumentException e) { continue; } @@ -871,54 +800,28 @@ private String extractBody(String skillMd) { return matcher.group(2).trim(); } - private SkillDetail readDetail(Path metadataPath) { - try { - return MAPPER.readValue(metadataPath.toFile(), SkillDetail.class); - } catch (IOException e) { - throw new IllegalStateException("Failed to read skill metadata: " + e.getMessage(), e); - } - } - - private void writeDetail(Path metadataPath, SkillDetail detail) { - try { - MAPPER.writerWithDefaultPrettyPrinter().writeValue(metadataPath.toFile(), detail); - } catch (IOException e) { - throw new IllegalStateException("Failed to write skill metadata: " + e.getMessage(), e); - } - } - private boolean packageExists(SkillDetail detail) { String handle = detail.getPackageFileHandleId(); - if (handle != null && !handle.isBlank()) { - try { - if (packageStore.exists(handle)) { - return true; - } - } catch (RuntimeException ignored) { - // Fall through to legacy package path lookup. - } + if (handle == null || handle.isBlank()) { + return false; + } + try { + return packageStore.exists(handle); + } catch (RuntimeException ignored) { + return false; } - return Files.exists(legacyPackagePath(detail.getOwnerId(), detail.getName(), detail.getVersion())); } private byte[] packageBytes(SkillDetail detail) { String handle = detail.getPackageFileHandleId(); - if (handle != null && !handle.isBlank()) { - try { - return packageStore.read(handle); - } catch (RuntimeException ignored) { - // Fall through to legacy package path lookup. - } - } - try { - return Files.readAllBytes(legacyPackagePath(detail.getOwnerId(), detail.getName(), detail.getVersion())); - } catch (IOException e) { + if (handle == null || handle.isBlank()) { throw new IllegalArgumentException( "Skill package not found: " + detail.getName() + "@" + detail.getVersion()); } + return packageStore.read(handle); } - private void deletePackage(SkillDetail detail) throws IOException { + private void deletePackage(SkillDetail detail) { String handle = detail.getPackageFileHandleId(); if (handle != null && !handle.isBlank()) { try { @@ -927,40 +830,6 @@ private void deletePackage(SkillDetail detail) throws IOException { // Legacy records used synthetic handles before the package store existed. } } - Files.deleteIfExists(legacyPackagePath(detail.getOwnerId(), detail.getName(), detail.getVersion())); - } - - private void updateLatestAfterDelete(String ownerId, String name) throws IOException { - Path skillRoot = skillRoot(ownerId, name); - if (!Files.isDirectory(skillRoot)) { - Files.deleteIfExists(latestPath(ownerId, name)); - return; - } - List remaining = new ArrayList<>(); - try (var versions = Files.list(skillRoot)) { - for (Path versionPath : versions.filter(Files::isDirectory).toList()) { - Path metadata = versionPath.resolve("metadata.json"); - if (Files.exists(metadata)) { - SkillDetail detail = readDetail(metadata); - if (isReadable(detail)) { - remaining.add(detail); - } - } - } - } - if (remaining.isEmpty()) { - Files.deleteIfExists(latestPath(ownerId, name)); - try (var children = Files.list(skillRoot)) { - if (children.findAny().isEmpty()) { - Files.deleteIfExists(skillRoot); - } - } - return; - } - remaining.sort(Comparator.comparing(SkillDetail::getCreatedAt, Comparator.nullsFirst(Long::compareTo)) - .thenComparing(SkillDetail::getVersion)); - Files.writeString( - latestPath(ownerId, name), remaining.get(remaining.size() - 1).getVersion(), StandardCharsets.UTF_8); } private String currentUserId() { @@ -994,38 +863,10 @@ private String normalizeEntryName(String name) { return normalized; } - private Path ownerRoot(String ownerId) { - return storageRoot.resolve("owners").resolve(encoded(ownerId)); - } - - private Path skillRoot(String ownerId, String name) { - return ownerRoot(ownerId).resolve(encoded(name)); - } - - private Path versionDir(String ownerId, String name, String version) { - return skillRoot(ownerId, name).resolve(encoded(version)); - } - - private Path metadataPath(String ownerId, String name, String version) { - return versionDir(ownerId, name, version).resolve("metadata.json"); - } - - private Path legacyPackagePath(String ownerId, String name, String version) { - return versionDir(ownerId, name, version).resolve("skill.zip"); - } - - private Path latestPath(String ownerId, String name) { - return skillRoot(ownerId, name).resolve("latest"); - } - private String packageStoreName(String ownerId, String name) { return ownerId + ":" + name; } - private String encoded(String value) { - return URLEncoder.encode(value, StandardCharsets.UTF_8); - } - private void validateSkillName(String name) { if (!SKILL_NAME_PATTERN.matcher(name).matches()) { throw new IllegalArgumentException( diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/spi/SkillMetadataDAO.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/spi/SkillMetadataDAO.java new file mode 100644 index 000000000..2eef5e92f --- /dev/null +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/spi/SkillMetadataDAO.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.spi; + +import java.util.List; +import java.util.Optional; + +import dev.agentspan.runtime.model.skill.SkillDetail; + +/** + * Persistence SPI for skill metadata (manifest, versions, and the per-skill "latest" + * pointer). Skill package bytes are stored separately via {@link SkillPackageStore}. + * + *

    The standalone server ships a filesystem-backed implementation (zero-config, single node); + * an embedding host (e.g. orkes-conductor) supplies a durable/HA implementation (e.g. Postgres) + * so skill listings are consistent across nodes.

    + * + *

    All operations are scoped by {@code ownerId}. Authorization (whether the caller may read a + * given skill) is the caller's concern, not this DAO's.

    + */ +public interface SkillMetadataDAO { + + /** + * Persist a skill version's metadata (create or overwrite). + * + * @param detail metadata to store, keyed by {@code ownerId + name + version} + * @param makeLatest when {@code true}, mark this version as the skill's latest + */ + void save(SkillDetail detail, boolean makeLatest); + + /** Exact-version lookup. */ + Optional find(String ownerId, String name, String version); + + /** The recorded latest version string for a skill, if any. */ + Optional latestVersion(String ownerId, String name); + + /** All recorded versions of a single skill (unordered). */ + List listVersions(String ownerId, String name); + + /** + * All skills for an owner. When {@code allVersions} is {@code false}, returns only each + * skill's latest version; when {@code true}, returns every version of every skill. + */ + List list(String ownerId, boolean allVersions); + + /** Remove a single version and recompute the skill's latest pointer if needed. */ + void delete(String ownerId, String name, String version); +} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/tasks/Join.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/tasks/Join.java index 2d41f4ad4..b1d8f92fe 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/tasks/Join.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/tasks/Join.java @@ -10,6 +10,7 @@ import java.util.Set; import java.util.stream.Collectors; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import com.netflix.conductor.annotations.VisibleForTesting; @@ -24,6 +25,7 @@ import lombok.extern.slf4j.Slf4j; @Component(TASK_TYPE_JOIN) +@ConditionalOnProperty(name = "agentspan.embedded", havingValue = "false", matchIfMissing = true) @Slf4j public class Join extends WorkflowSystemTask { diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/EmbeddedMode.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/EmbeddedMode.java new file mode 100644 index 000000000..6c05757d4 --- /dev/null +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/EmbeddedMode.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.util; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * Captures whether AgentSpan is running embedded in a host (e.g. orkes-conductor) into a static + * flag, so compile-time code reached through plain (non-Spring) helpers — e.g. {@code ToolCompiler} + * — can branch on it. Populated once at startup from the {@code agentspan.embedded} property + * (default {@code false} for the standalone server). Compilation is request-driven, so the value is + * always set before any compile runs. + */ +@Component +public class EmbeddedMode { + + private static volatile boolean embedded = false; + + @Value("${agentspan.embedded:false}") + public void setEmbedded(boolean value) { + embedded = value; + } + + public static boolean isEmbedded() { + return embedded; + } +} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/ProviderValidator.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/ProviderValidator.java index df7b22f11..440362e92 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/ProviderValidator.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/ProviderValidator.java @@ -7,6 +7,7 @@ import java.util.Optional; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import dev.agentspan.runtime.ai.AgentspanAIModelProvider; @@ -21,12 +22,24 @@ public class ProviderValidator { private static final String DOCS_URL = "https://github.com/agentspan-ai/agentspan/blob/main/docs/ai-models.md"; + /** + * When embedded in a host (e.g. orkes-conductor), Conductor is the authority for model + * providers and credentials: conductor-ai integrations resolve providers by name, + * and the host's credential store (AWS SSM / Vault / etc., reached via the + * {@code CredentialStoreProvider} SPI) supplies raw keys. This standalone pre-flight check + * only knows AgentSpan's own provider model, so it would wrongly reject host-configured + * providers. The execution path already delegates to conductor-ai (which resolves or + * rejects the provider), so when embedded we defer to Conductor and skip this check. + */ + @Value("${agentspan.embedded:false}") + private boolean embedded; + /** * Returns Optional.empty() if the provider is configured (either via startup environment * variables or via a credential added in the UI), or Optional.of(errorMessage) if not. */ public Optional validateProvider(String provider) { - if (aiModelProvider.isProviderConfigured(provider)) { + if (embedded || aiModelProvider.isProviderConfigured(provider)) { return Optional.empty(); } return Optional.of("Model provider '" + provider + "' is not configured. " From a0c71a90363604f0dd9427d97ea710a8c7bb7c0a Mon Sep 17 00:00:00 2001 From: bradyyie Date: Thu, 11 Jun 2026 12:36:23 -0400 Subject: [PATCH 20/61] Spotless --- .../main/java/dev/agentspan/runtime/service/AgentService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java index fa110d6ba..8e82e3440 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java @@ -296,7 +296,8 @@ public StartResponse start(StartRequest request) { // impersonates it during decide so sub-workflows inherit attribution, and its // workers' poll-time secret substitution REQUIRES it (tasks of workflows without // createdBy fail to poll). Stock Conductor simply records the value. - String principal = RequestContextHolder.get().map(ctx -> ctx.getUserId()).orElse(null); + String principal = + RequestContextHolder.get().map(ctx -> ctx.getUserId()).orElse(null); if (principal != null) { startReq.setCreatedBy(principal); } From 6c0c9e0eba636fba96b901dbbd93172c13d318ad Mon Sep 17 00:00:00 2001 From: bradyyie Date: Thu, 11 Jun 2026 12:41:41 -0400 Subject: [PATCH 21/61] Spotless --- .../service/skill/FileSystemSkillMetadataDAO.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java index 6d3077a46..711dce57c 100644 --- a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java +++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java @@ -51,9 +51,7 @@ public void save(SkillDetail detail, boolean makeLatest) { writeDetail(metadataPath, detail); if (makeLatest) { Files.writeString( - latestPath(detail.getOwnerId(), detail.getName()), - detail.getVersion(), - StandardCharsets.UTF_8); + latestPath(detail.getOwnerId(), detail.getName()), detail.getVersion(), StandardCharsets.UTF_8); } } catch (IOException e) { throw new IllegalStateException("Failed to write skill metadata: " + e.getMessage(), e); @@ -113,7 +111,8 @@ public List list(String ownerId, boolean allVersions) { for (Path skillDir : skillDirs.filter(Files::isDirectory).toList()) { if (allVersions) { try (var versions = Files.list(skillDir)) { - for (Path versionPath : versions.filter(Files::isDirectory).toList()) { + for (Path versionPath : + versions.filter(Files::isDirectory).toList()) { Path metadata = versionPath.resolve("metadata.json"); if (Files.exists(metadata)) { details.add(readDetail(metadata)); @@ -123,7 +122,8 @@ public List list(String ownerId, boolean allVersions) { } else { Path latest = skillDir.resolve("latest"); if (Files.exists(latest)) { - String version = Files.readString(latest, StandardCharsets.UTF_8).trim(); + String version = + Files.readString(latest, StandardCharsets.UTF_8).trim(); Path metadata = skillDir.resolve(encoded(version)).resolve("metadata.json"); if (Files.exists(metadata)) { details.add(readDetail(metadata)); @@ -149,7 +149,8 @@ public void delete(String ownerId, String name, String version) { } Path latest = latestPath(ownerId, name); if (Files.exists(latest) - && version.equals(Files.readString(latest, StandardCharsets.UTF_8).trim())) { + && version.equals( + Files.readString(latest, StandardCharsets.UTF_8).trim())) { updateLatestAfterDelete(ownerId, name); } } catch (IOException e) { From cb63fbc2ff4bad2f279ef3f323deddc6a3545f32 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Thu, 11 Jun 2026 10:48:23 -0700 Subject: [PATCH 22/61] refactor(compiler): source auto-exposed tools from RegisteredAgent beans, not a DAO scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AutoExposedToolsMerger previously discovered auto-exposed sub-agents by scanning the metadata store for WorkflowDefs stamped with a metadata marker, caching the result forever. The data it was reading back came from the same Spring context: the registrar wrote RegisteredAgent beans into the DAO, then the merger scanned them back out. That round-trip is what created the bootstrap-ordering trap (a compile during @PostConstruct froze an empty cache for the bean's lifetime, silently hiding every registered agent — see 879851f8) plus the lazy cache, the transient-failure retry carve-out, and the untyped metadata parsing. Now the merger builds its entries from List at construction, so it is complete before any compile can run and the trap is structurally impossible. Deleted along the way: - the AUTO_EXPOSE_AS_TOOL_METADATA_KEY wire protocol (stamping in the registrar, parsing in the merger, the re-export on AgentCompiler) - the volatile cache + double-checked locking + failure-retry logic - AgentCompiler's MetadataDAO constructor and mergeAutoExposedTools delegate The registrar still persists each WorkflowDef — that's needed for SUB_WORKFLOW dispatch by name — but it's now a dumb compile-and-persist loop. A blank autoExpose tool name now fails fast at boot instead of being silently skipped. Tests rewritten to the bean-list model; the obsolete cache-behavior tests are replaced by construction-time pins. Validated per CLAUDE.md by mutating merge() into a no-op: exactly the injection-asserting tests failed, guards stayed green. Co-Authored-By: Claude Fable 5 --- docs/ocg-agent-flow.md | 44 ++-- .../runtime/compiler/AgentCompiler.java | 67 ++--- .../compiler/AutoExposedToolsMerger.java | 148 +++-------- .../runtime/registry/RegisteredAgent.java | 16 +- .../registry/RegisteredAgentRegistrar.java | 35 +-- .../compiler/AutoExposedToolsMergeTest.java | 237 +++++++++--------- .../RegisteredAgentBootstrapTest.java | 79 +++--- .../RegisteredAgentRegistrarTest.java | 62 +---- 8 files changed, 265 insertions(+), 423 deletions(-) diff --git a/docs/ocg-agent-flow.md b/docs/ocg-agent-flow.md index 362b8eb56..da1be1a38 100644 --- a/docs/ocg-agent-flow.md +++ b/docs/ocg-agent-flow.md @@ -94,7 +94,7 @@ state. ```mermaid flowchart TB subgraph L4["Layer 4 — Generic auto-expose (OCG-agnostic)"] - AC["AgentCompiler
    .mergeAutoExposedTools
    .AUTO_EXPOSE_AS_TOOL_METADATA_KEY"] + AC["AgentCompiler +
    AutoExposedToolsMerger
    (reads RegisteredAgent beans)"] RAR["RegisteredAgentRegistrar
    (picks up RegisteredAgent beans)"] RTR["RegisteredTaskDefsRegistrar
    (picks up RegisteredTaskDefs beans)"] end @@ -152,22 +152,27 @@ sequenceDiagram deactivate TDR SB->>+AR: @PostConstruct - AR->>+OcgBeans: agentConfig() + autoExpose() - OcgBeans-->>-AR: AgentConfig("_ocg_agent") +
    ExposeAsTool("ocg_agent", "Delegate to…") - AR->>+AC: compile(AgentConfig) + AR->>+OcgBeans: agentConfig() + OcgBeans-->>-AR: AgentConfig("_ocg_agent") + AR->>+AC: compileWithoutAutoExpose(AgentConfig) AC-->>-AR: WorkflowDef "_ocg_agent" - AR->>AR: stamp def.metadata[autoExposeAsTool]
    = {name, description} AR->>+DAO: updateWorkflowDef DAO-->>-AR: ok deactivate AR - Note over DAO: _ocg_agent is now dispatchable
    AND auto-exposed to every
    top-level user agent + Note over DAO: _ocg_agent is now dispatchable
    by name at runtime ``` The registrars know nothing about OCG. They iterate `List` and `List` provided by Spring, run each through a -fixed pipeline (compile + stamp + persist for agents; persist for -TaskDefs), and call it a day. OCG just happens to be the one feature -providing those beans today. +fixed pipeline (compile + persist for agents; persist for TaskDefs), and +call it a day. OCG just happens to be the one feature providing those +beans today. + +LLM visibility is handled separately: `AutoExposedToolsMerger` reads +`autoExpose()` straight from the same `RegisteredAgent` bean list at +construction, so user compiles see `ocg_agent` regardless of when these +DAO writes happen — there is no startup-ordering dependency between +registration and visibility. --- @@ -185,13 +190,10 @@ sequenceDiagram AS->>AS: resolveConfig() — normalize framework AS->>+AC: compile(config) - Note over AC: mergeAutoExposedTools(config) - AC->>+DAO: getAllWorkflowDefsLatestVersions() - DAO-->>-AC: every WorkflowDef in the store - loop for each def - AC->>AC: readAutoExposeSpec(def)
    (returns null unless flagged) - alt has marker, name != config.name, not duplicate - AC->>AC: append ToolConfig{
    name: spec.name
    toolType: "agent_tool"
    config.workflowName: def.name
    } + Note over AC: AutoExposedToolsMerger.merge(config)
    entries fixed at construction from
    the RegisteredAgent bean list — no DAO read + loop for each auto-exposed RegisteredAgent + alt name != config.name, not duplicate + AC->>AC: append ToolConfig{
    name: expose.toolName
    toolType: "agent_tool"
    config.workflowName: agent workflow name
    } end end @@ -206,11 +208,11 @@ sequenceDiagram Guards inside the merger: -| Guard | Why | -| ---------------- | ------------------------------------------------------------------ | -| No `MetadataDAO` | Unit tests using `new AgentCompiler()` should still work | -| Self-recursion | Re-compiling `_ocg_agent` itself won't inject itself as a tool | -| Duplicate name | A caller's explicit declaration wins | +| Guard | Why | +| -------------------------- | ------------------------------------------------------------------ | +| No `RegisteredAgent` beans | Unit tests using `new AgentCompiler()` should still work | +| Self-recursion | Re-compiling `_ocg_agent` itself won't inject itself as a tool | +| Duplicate name | A caller's explicit declaration wins | --- diff --git a/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java b/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java index ea03a1893..444c00d9d 100644 --- a/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java +++ b/server/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java @@ -17,7 +17,6 @@ import com.netflix.conductor.common.metadata.workflow.SubWorkflowParams; import com.netflix.conductor.common.metadata.workflow.WorkflowDef; import com.netflix.conductor.common.metadata.workflow.WorkflowTask; -import com.netflix.conductor.dao.MetadataDAO; import dev.agentspan.runtime.model.*; import dev.agentspan.runtime.util.JavaScriptBuilder; @@ -41,50 +40,31 @@ public class AgentCompiler { "message", "${workflow.input.prompt}", "media", "${workflow.input.media}"); - /** - * Metadata key on a {@link WorkflowDef} that marks the workflow as one - * that should be silently appended to every top-level agent's tool list - * at compile time. Re-exported from {@link AutoExposedToolsMerger} so - * callers (registrar, tests) that previously imported it from this class - * keep compiling without churn. - */ - public static final String AUTO_EXPOSE_AS_TOOL_METADATA_KEY = - AutoExposedToolsMerger.AUTO_EXPOSE_AS_TOOL_METADATA_KEY; - private int timeoutSeconds = 0; private int llmRetryCount = 3; private int contextMaxSizeBytes = 32768; private int contextMaxValueSizeBytes = 4096; /** - * Owns the DAO scan, cache, and per-config merge of auto-exposed - * server-registered agents. Pulled out so {@code AgentCompiler} stays - * focused on workflow compilation and the merge lifecycle (cache, DAO - * failure handling, self-recursion guard) lives in one place. + * Owns the per-config merge of auto-exposed server-registered agents. + * Pulled out so {@code AgentCompiler} stays focused on workflow + * compilation; the merger sources its entries from the + * {@code RegisteredAgent} bean list at construction. */ private final AutoExposedToolsMerger autoExposedMerger; /** - * Default no-arg constructor for tests that don't need a {@link MetadataDAO}. - * The auto-expose merge becomes a no-op. + * Default no-arg constructor for tests that don't need the auto-expose + * merge — it becomes a no-op. */ public AgentCompiler() { this(AutoExposedToolsMerger.disabled()); } - /** - * Convenience constructor for tests that want to exercise the merge - * against a stubbed {@link MetadataDAO} without wiring up an - * {@link AutoExposedToolsMerger} explicitly. - */ - public AgentCompiler(MetadataDAO metadataDAO) { - this(new AutoExposedToolsMerger(metadataDAO)); - } - /** * Spring-injected constructor. The merger is itself a {@code @Component} - * that takes an optional {@link MetadataDAO}, so this is the path the - * runtime uses. + * built from the {@code RegisteredAgent} bean list, so this is the path + * the runtime uses. */ @Autowired public AgentCompiler(AutoExposedToolsMerger autoExposedMerger) { @@ -139,31 +119,22 @@ String getText() { * Public entry point: compile a top-level {@link AgentConfig} into a * {@link WorkflowDef}. * - *

    Before strategy dispatch, any workflow registered in the metadata - * store with the {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} flag is - * silently appended to {@code config.tools} as an {@code agent_tool}. - * That's how the OCG sub-agent (and any future server-side sub-agent) - * becomes LLM-visible without per-feature injection code.

    + *

    Before strategy dispatch, every auto-exposed + * {@code RegisteredAgent} is silently appended to {@code config.tools} + * as an {@code agent_tool}. That's how the OCG sub-agent (and any + * future server-side sub-agent) becomes LLM-visible without + * per-feature injection code.

    * *

    Internal recursion (nested sub-agents, graph-structure sub-compiles, * MultiAgentCompiler swarm) must go through {@link #compileWithoutAutoExpose} * instead so a specialist sub-agent isn't polluted with an unrelated - * retrieval tool and the DAO isn't queried for every level of the tree.

    + * retrieval tool.

    */ public WorkflowDef compile(AgentConfig config) { autoExposedMerger.merge(config); return compileWithoutAutoExpose(config); } - /** - * Thin delegate so existing tests calling {@code compiler.mergeAutoExposedTools(config)} - * continue to compile. New code should go through the injected - * {@link AutoExposedToolsMerger} directly. - */ - void mergeAutoExposedTools(AgentConfig config) { - autoExposedMerger.merge(config); - } - /** * Compile entry that performs strategy dispatch and post-processing * without running the auto-expose merge. @@ -174,14 +145,8 @@ void mergeAutoExposedTools(AgentConfig config) { * compile, {@code MultiAgentCompiler}) — nested specialist agents * must not inherit unrelated server-side tools. *
  • {@link dev.agentspan.runtime.registry.RegisteredAgentRegistrar} - * at {@code @PostConstruct} time — registered server agents don't - * need other registered agents auto-exposed to them, AND skipping - * the merge here is what keeps the merger's lazy cache from - * snapshotting an empty list during the bootstrap loop. Letting - * the registrar trigger the merge before its own - * {@code dao.updateWorkflowDef} write would freeze the cache to - * an empty result for the life of the bean, silently hiding every - * server-registered agent from subsequent user compiles.
  • + * at {@code @PostConstruct} time — registered server agents + * shouldn't have other registered agents auto-exposed to them. * */ public WorkflowDef compileWithoutAutoExpose(AgentConfig config) { diff --git a/server/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java b/server/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java index 6a168a582..c8ce483ad 100644 --- a/server/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java +++ b/server/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java @@ -16,92 +16,69 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import com.netflix.conductor.common.metadata.workflow.WorkflowDef; -import com.netflix.conductor.dao.MetadataDAO; - import dev.agentspan.runtime.model.AgentConfig; import dev.agentspan.runtime.model.ToolConfig; +import dev.agentspan.runtime.registry.RegisteredAgent; +import dev.agentspan.runtime.registry.RegisteredAgent.ExposeAsTool; /** * Owns the "auto-expose registered sub-agents as tools" contract for * {@link AgentCompiler}. * - *

    The contract: any workflow registered in Conductor's metadata store - * carrying the {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} marker is silently - * appended to every top-level agent's tool list as an {@code agent_tool} - * at compile time. The OCG sub-agent (and any future server-registered - * sub-agent) becomes LLM-visible via this single mechanism — no per-feature - * injection class required.

    - * - *

    Lifecycle: the result of the DAO scan is cached on first successful - * read and never refreshed. Registered server-side agents are written at - * server {@code @PostConstruct} time and don't change at runtime, so - * re-querying the DAO per compile would be wasted work. A transient DAO - * failure is not cached — the next compile retries the lookup so - * a one-shot blip doesn't permanently hide registered agents.

    + *

    The contract: any {@link RegisteredAgent} bean whose + * {@link RegisteredAgent#autoExpose()} is non-null is silently appended to + * every top-level agent's tool list as an {@code agent_tool} at compile + * time. The OCG sub-agent (and any future server-registered sub-agent) + * becomes LLM-visible via this single mechanism — no per-feature injection + * class required.

    * - *

    Bootstrap ordering: the registrar that writes auto-exposed agents - * must call {@link AgentCompiler#compileWithoutAutoExpose} during its - * {@code @PostConstruct} loop, never {@link AgentCompiler#compile}. - * Triggering the merge before the registrar finishes its - * {@code dao.updateWorkflowDef} writes would snapshot an empty list and - * freeze it for the bean's lifetime.

    + *

    The entries are read straight from the Spring-managed bean list at + * construction time. The beans are also what {@code RegisteredAgentRegistrar} + * persists to the metadata store, so both sides share one source of truth + * and no DAO read-back is needed: the merger is complete the moment it is + * constructed, regardless of when the registrar's {@code @PostConstruct} + * writes happen.

    */ @Component public class AutoExposedToolsMerger { - /** - * Metadata key on a {@link WorkflowDef} that marks the workflow as one - * that should be silently appended to every top-level agent's tool list - * at compile time. The value is a {@code Map} with at - * least {@code name} and {@code description}; both are surfaced to the - * agent's LLM via the {@code agent_tool} routing. - */ - public static final String AUTO_EXPOSE_AS_TOOL_METADATA_KEY = "agentspan.autoExposeAsTool"; - private static final Logger log = LoggerFactory.getLogger(AutoExposedToolsMerger.class); /** - * Optional — null for unit tests that construct {@link AgentCompiler} - * directly. When null, {@link #merge(AgentConfig)} is a no-op. + * Pairing of source workflow name + pre-built {@code agent_tool} + * {@link ToolConfig}, fixed at construction. The workflow name rides + * alongside the tool so the per-compile self-recursion guard works + * without re-reading the {@link AgentConfig}. */ - private final MetadataDAO metadataDAO; + private record AutoExposedEntry(String workflowName, ToolConfig tool) {} - /** - * Lazy cache of the auto-exposed tool entries the DAO returns. Populated - * on first successful {@link #autoExposedEntries()} call and never - * refreshed. {@code volatile} for the double-checked-locking idiom. - */ - private volatile List cachedAutoExposed; + private final List entries; @Autowired - public AutoExposedToolsMerger(@Autowired(required = false) MetadataDAO metadataDAO) { - this.metadataDAO = metadataDAO; + public AutoExposedToolsMerger(@Autowired(required = false) List registeredAgents) { + this.entries = buildEntries(registeredAgents != null ? registeredAgents : List.of()); } - /** A no-DAO merger that is always a no-op. For tests / direct construction. */ + /** A merger with no registered agents — always a no-op. For tests / direct construction. */ public static AutoExposedToolsMerger disabled() { return new AutoExposedToolsMerger(null); } /** - * Append every DAO-registered workflow that carries the - * {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} marker to {@code config.tools} + * Append every auto-exposed registered agent to {@code config.tools} * as an {@code agent_tool}. * *

    Mutates {@code config} in place. Skips when:

    *
      - *
    • {@link #metadataDAO} is absent (unit-test path)
    • - *
    • The workflow being compiled IS the auto-exposed one + *
    • No {@link RegisteredAgent} bean requested auto-expose
    • + *
    • The agent being compiled IS the auto-exposed one * — no self-recursion
    • *
    • A tool with that name is already declared on the config * — caller's explicit declaration wins
    • *
    */ public void merge(AgentConfig config) { - if (config == null) return; - List entries = autoExposedEntries(); - if (entries.isEmpty()) return; + if (config == null || entries.isEmpty()) return; Set takenNames = collectToolNames(config); List toAppend = new ArrayList<>(); @@ -120,52 +97,19 @@ public void merge(AgentConfig config) { } } - /** Typed view of an {@link #AUTO_EXPOSE_AS_TOOL_METADATA_KEY} entry. */ - private record AutoExposeSpec(String toolName, String description) {} - - /** - * Cached pairing of source workflow name + pre-built {@code agent_tool} - * {@link ToolConfig}. The workflow name rides alongside the tool so the - * per-compile self-recursion guard stays correct without re-reading - * {@link WorkflowDef} metadata. - */ - private record AutoExposedEntry(String workflowName, ToolConfig tool) {} - - private List autoExposedEntries() { - List snapshot = cachedAutoExposed; - if (snapshot != null) return snapshot; - if (metadataDAO == null) { - cachedAutoExposed = List.of(); - return cachedAutoExposed; - } - synchronized (this) { - if (cachedAutoExposed != null) return cachedAutoExposed; - try { - List defs = metadataDAO.getAllWorkflowDefsLatestVersions(); - List built = buildEntries(defs == null ? List.of() : defs); - cachedAutoExposed = built; - log.debug( - "auto-expose merge: fetched {} workflow def(s); cached {} auto-exposed entry(ies)", - defs == null ? 0 : defs.size(), - built.size()); - return built; - } catch (Exception e) { - // NOT cached — let the next compile retry the DAO. - log.warn( - "auto-expose merge: metadataDAO lookup failed; will retry on next compile. {}", e.getMessage()); - return List.of(); - } - } - } - - private static List buildEntries(List defs) { + private static List buildEntries(List registeredAgents) { List built = new ArrayList<>(); - for (WorkflowDef def : defs) { - AutoExposeSpec spec = readAutoExposeSpec(def); - if (spec == null) continue; - built.add(new AutoExposedEntry(def.getName(), buildAgentTool(def.getName(), spec))); + for (RegisteredAgent agent : registeredAgents) { + ExposeAsTool expose = agent.autoExpose(); + if (expose == null) continue; + if (expose.toolName() == null || expose.toolName().isBlank()) { + throw new IllegalStateException("RegisteredAgent " + + agent.getClass().getName() + " requested auto-expose with a blank tool name"); + } + String workflowName = agent.agentConfig().getName(); + built.add(new AutoExposedEntry(workflowName, buildAgentTool(workflowName, expose))); } - return built; + return List.copyOf(built); } private static Set collectToolNames(AgentConfig config) { @@ -177,21 +121,11 @@ private static Set collectToolNames(AgentConfig config) { return names; } - private static AutoExposeSpec readAutoExposeSpec(WorkflowDef def) { - Map metadata = def.getMetadata(); - if (metadata == null || !(metadata.get(AUTO_EXPOSE_AS_TOOL_METADATA_KEY) instanceof Map spec)) { - return null; - } - if (!(spec.get("name") instanceof String toolName) || toolName.isEmpty()) return null; - String description = spec.get("description") instanceof String s ? s : ""; - return new AutoExposeSpec(toolName, description); - } - - private static ToolConfig buildAgentTool(String workflowName, AutoExposeSpec spec) { + private static ToolConfig buildAgentTool(String workflowName, ExposeAsTool expose) { return ToolConfig.builder() - .name(spec.toolName()) + .name(expose.toolName()) .toolType("agent_tool") - .description(spec.description()) + .description(expose.toolDescription() != null ? expose.toolDescription() : "") .config(Map.of("workflowName", workflowName)) .build(); } diff --git a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java index 1f130af8b..c63f8fd2f 100644 --- a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java +++ b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java @@ -15,10 +15,12 @@ * {@code WorkflowDef}, and persisted to Conductor's metadata store. Adding * a new server-side sub-agent is therefore a one-bean change — no * per-feature {@code @PostConstruct}, no manual {@code MetadataDAO} - * write, no duplication of the compile/stamp ceremony.

    + * write, no duplication of the compile/persist ceremony.

    * *

    Implementations should be stateless or read configuration via Spring - * injection. {@link #agentConfig()} is invoked exactly once at startup.

    + * injection. {@link #agentConfig()} must be pure — it is invoked at + * startup by both the registrar (to compile and persist) and + * {@code AutoExposedToolsMerger} (to read the workflow name).

    */ public interface RegisteredAgent { @@ -30,11 +32,11 @@ public interface RegisteredAgent { AgentConfig agentConfig(); /** - * When non-null, the registrar stamps an auto-expose marker on the - * compiled {@code WorkflowDef} so {@code AgentCompiler.mergeAutoExposedTools} - * appends this agent as an {@code agent_tool} on every top-level - * user-agent compile. Return {@code null} to register the workflow - * without exposing it as a tool. + * When non-null, {@code AutoExposedToolsMerger} reads this spec + * directly from the bean and appends the agent as an + * {@code agent_tool} on every top-level user-agent compile. Return + * {@code null} to register the workflow without exposing it as a + * tool. */ default ExposeAsTool autoExpose() { return null; diff --git a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java index 29c34cc23..367f1a90f 100644 --- a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java +++ b/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java @@ -5,9 +5,7 @@ package dev.agentspan.runtime.registry; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import jakarta.annotation.PostConstruct; @@ -26,13 +24,17 @@ /** * Generic registrar that drives every {@link RegisteredAgent} bean through - * the same compile → (optional auto-expose stamp) → persist pipeline on - * server startup. + * the same compile → persist pipeline on server startup. * *

    Replaces feature-specific {@code @PostConstruct registerWorkflow()} * methods that previously coupled OCG (and future sub-agents) to the * mechanics of metadata-store writes. Adding a new server-side sub-agent * now only requires declaring a {@code @Bean RegisteredAgent}.

    + * + *

    LLM visibility is not this class's job: {@code AutoExposedToolsMerger} + * reads {@link RegisteredAgent#autoExpose()} straight from the bean list, + * so the persisted {@link WorkflowDef} only needs to exist for + * SUB_WORKFLOW dispatch to resolve it by name at runtime.

    */ @Component @DependsOn("registeredTaskDefsRegistrar") @@ -66,32 +68,15 @@ public void registerAll() { private void register(RegisteredAgent agent) { AgentConfig config = agent.agentConfig(); - // ``compileWithoutAutoExpose`` (not ``compile``) for two reasons: - // 1. Registered agents shouldn't have other registered agents - // auto-injected into them as tools. - // 2. The merger's lazy cache must not be triggered here — the - // registered defs aren't in the DAO yet at this point, so the - // first read would snapshot an empty list and freeze it for the - // bean's lifetime, silently hiding every registered agent from - // subsequent user compiles. + // ``compileWithoutAutoExpose`` (not ``compile``) — registered agents + // shouldn't have other registered agents auto-injected into them as + // tools. WorkflowDef def = agentCompiler.compileWithoutAutoExpose(config); - ExposeAsTool expose = agent.autoExpose(); - if (expose != null) { - stampAutoExpose(def, expose); - } metadataDAO.updateWorkflowDef(def); + ExposeAsTool expose = agent.autoExpose(); log.info( "Registered agent: workflow='{}'{}", def.getName(), expose != null ? " autoExposeAs='" + expose.toolName() + "'" : ""); } - - private static void stampAutoExpose(WorkflowDef def, ExposeAsTool expose) { - Map metadata = - def.getMetadata() != null ? new LinkedHashMap<>(def.getMetadata()) : new LinkedHashMap<>(); - metadata.put( - AgentCompiler.AUTO_EXPOSE_AS_TOOL_METADATA_KEY, - Map.of("name", expose.toolName(), "description", expose.toolDescription())); - def.setMetadata(metadata); - } } diff --git a/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java b/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java index d4b4d269e..39e2e3fc5 100644 --- a/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java +++ b/server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java @@ -6,103 +6,91 @@ package dev.agentspan.runtime.compiler; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.netflix.conductor.common.metadata.workflow.WorkflowDef; -import com.netflix.conductor.dao.MetadataDAO; - import dev.agentspan.runtime.model.AgentConfig; import dev.agentspan.runtime.model.ToolConfig; +import dev.agentspan.runtime.registry.RegisteredAgent; +import dev.agentspan.runtime.registry.RegisteredAgent.ExposeAsTool; /** - * Unit tests for {@link AgentCompiler#mergeAutoExposedTools}. + * Unit tests for {@link AutoExposedToolsMerger}. * *

    Pins the generic auto-expose mechanism that lets any server-side - * sub-agent become LLM-visible to every top-level agent just by stamping - * a metadata flag on its {@link WorkflowDef}. These tests are intentionally - * not OCG-specific — OCG is one consumer; the contract here is the - * one every future consumer relies on.

    + * sub-agent become LLM-visible to every top-level agent just by declaring + * a {@link RegisteredAgent} bean with a non-null {@code autoExpose()}. + * These tests are intentionally not OCG-specific — OCG is one consumer; + * the contract here is the one every future consumer relies on.

    */ class AutoExposedToolsMergeTest { - private AgentCompiler compiler; - private MetadataDAO metadataDAO; - - @BeforeEach - void setUp() { - metadataDAO = mock(MetadataDAO.class); - compiler = new AgentCompiler(metadataDAO); - } - @Test - void appendsFlaggedWorkflowAsAgentTool() { - WorkflowDef flagged = wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "Use this when you need help."); - when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(flagged)); + void appendsRegisteredAgentAsAgentTool() { + AutoExposedToolsMerger merger = new AutoExposedToolsMerger( + List.of(registeredAgent("_helper_agent", "helper_agent", "Use this when you need help."))); AgentConfig config = AgentConfig.builder() .name("user_agent") .tools(new ArrayList<>(List.of(workerTool("search")))) .build(); - compiler.mergeAutoExposedTools(config); + merger.merge(config); assertThat(config.getTools()).hasSize(2); ToolConfig injected = config.getTools().get(1); assertThat(injected.getName()).isEqualTo("helper_agent"); assertThat(injected.getToolType()).isEqualTo("agent_tool"); - // workflowName must match the registered WorkflowDef so SUB_WORKFLOW - // dispatch at runtime resolves to the right workflow — drift here - // would silently route to a missing workflow. + // workflowName must match the WorkflowDef the registrar persists so + // SUB_WORKFLOW dispatch at runtime resolves to the right workflow — + // drift here would silently route to a missing workflow. assertThat(injected.getConfig()).containsEntry("workflowName", "_helper_agent"); assertThat(injected.getDescription()).isEqualTo("Use this when you need help."); } @Test - void skipsWorkflowsWithoutTheMetadataKey() { - WorkflowDef plain = new WorkflowDef(); - plain.setName("_unrelated_workflow"); - plain.setMetadata(Map.of("some_other_key", "value")); - when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(plain)); + void skipsAgentsThatDoNotRequestAutoExpose() { + // autoExpose() == null means "register the workflow but keep it + // invisible to user agents" (private helper, internal pipeline). + RegisteredAgent privateAgent = new RegisteredAgent() { + @Override + public AgentConfig agentConfig() { + return AgentConfig.builder().name("_private_agent").build(); + } + }; + AutoExposedToolsMerger merger = new AutoExposedToolsMerger(List.of(privateAgent)); AgentConfig config = AgentConfig.builder() .name("user_agent") .tools(new ArrayList<>(List.of(workerTool("search")))) .build(); - compiler.mergeAutoExposedTools(config); + merger.merge(config); - // Only the user-declared tool remains; the unflagged workflow is - // invisible to the merger. assertThat(config.getTools()).hasSize(1); assertThat(config.getTools().get(0).getName()).isEqualTo("search"); } @Test void doesNotInjectIntoTheAutoExposedWorkflowItself() { - // Self-recursion guard: if the compile target IS the flagged - // workflow, the merger must skip it. Without this, the OCG agent's - // own compile would see itself in the DAO and recursively gain - // itself as a tool — broken tool list + infinite delegation. - WorkflowDef flagged = wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "irrelevant"); - when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(flagged)); + // Self-recursion guard: if the compile target IS the registered + // agent, the merger must skip it. Without this, the OCG agent's own + // compile would gain itself as a tool — broken tool list + infinite + // delegation. + AutoExposedToolsMerger merger = + new AutoExposedToolsMerger(List.of(registeredAgent("_helper_agent", "helper_agent", "irrelevant"))); AgentConfig config = AgentConfig.builder() .name("_helper_agent") .tools(new ArrayList<>(List.of(workerTool("internal_tool")))) .build(); - compiler.mergeAutoExposedTools(config); + merger.merge(config); assertThat(config.getTools()).hasSize(1); assertThat(config.getTools().get(0).getName()).isEqualTo("internal_tool"); @@ -113,8 +101,8 @@ void doesNotDuplicateWhenAToolWithThatNameAlreadyExists() { // The caller's explicit declaration wins. Two entries with the // same name would confuse the LLM's tool spec list and cause both // dispatches to resolve to the same workflow. - WorkflowDef flagged = wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "auto description"); - when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(flagged)); + AutoExposedToolsMerger merger = new AutoExposedToolsMerger( + List.of(registeredAgent("_helper_agent", "helper_agent", "auto description"))); ToolConfig existing = ToolConfig.builder() .name("helper_agent") @@ -126,43 +114,43 @@ void doesNotDuplicateWhenAToolWithThatNameAlreadyExists() { .tools(new ArrayList<>(List.of(existing))) .build(); - compiler.mergeAutoExposedTools(config); + merger.merge(config); assertThat(config.getTools()).hasSize(1); assertThat(config.getTools().get(0).getDescription()).isEqualTo("user-provided description"); } @Test - void noOpWhenMetadataDaoIsAbsent() { - // Tests that construct AgentCompiler without a metadataDAO must - // continue to work. The no-arg constructor exists for this path; - // the merger short-circuits cleanly instead of NPE-ing. - AgentCompiler noDao = new AgentCompiler(); // null metadataDAO + void noOpWhenNoRegisteredAgentsArePresent() { + // The no-arg AgentCompiler constructor (used throughout the test + // suite) wires a disabled merger — compiles must work untouched. + AgentCompiler compiler = new AgentCompiler(); AgentConfig config = AgentConfig.builder() .name("user_agent") + .model("openai/gpt-4o-mini") .tools(new ArrayList<>(List.of(workerTool("search")))) .build(); - noDao.mergeAutoExposedTools(config); + compiler.compile(config); assertThat(config.getTools()).hasSize(1); } @Test - void appendsMultipleFlaggedWorkflowsInDaoOrder() { - WorkflowDef a = wfWithAutoExposeMetadata("_a_agent", "alpha_agent", "first"); - WorkflowDef b = wfWithAutoExposeMetadata("_b_agent", "beta_agent", "second"); - when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(a, b)); + void appendsMultipleAgentsInBeanOrder() { + AutoExposedToolsMerger merger = new AutoExposedToolsMerger(List.of( + registeredAgent("_a_agent", "alpha_agent", "first"), + registeredAgent("_b_agent", "beta_agent", "second"))); AgentConfig config = AgentConfig.builder() .name("user_agent") .tools(new ArrayList<>()) .build(); - compiler.mergeAutoExposedTools(config); + merger.merge(config); - // Both flagged workflows surface as agent_tools on the same config. + // Both registered agents surface as agent_tools on the same config. // This is the path that lets future server-side capabilities // accumulate without any per-feature wiring. assertThat(config.getTools()).hasSize(2); @@ -170,6 +158,25 @@ void appendsMultipleFlaggedWorkflowsInDaoOrder() { assertThat(config.getTools().get(1).getName()).isEqualTo("beta_agent"); } + @Test + void compileEntryRunsTheMerge() { + // The public ``compile()`` is the single place user agents pick up + // auto-exposed tools — pin that the wiring from AgentCompiler into + // the merger actually fires. + AgentCompiler compiler = new AgentCompiler( + new AutoExposedToolsMerger(List.of(registeredAgent("_helper_agent", "helper_agent", "x")))); + + AgentConfig config = AgentConfig.builder() + .name("user_agent") + .model("openai/gpt-4o-mini") + .tools(new ArrayList<>()) + .build(); + + compiler.compile(config); + + assertThat(config.getTools()).extracting(ToolConfig::getName).contains("helper_agent"); + } + @Test void mergeRunsOnceAtTopLevelOnlyAndSkipsInternalRecursion() { // Pinning the contract that the public ``compile()`` is the only entry @@ -177,8 +184,8 @@ void mergeRunsOnceAtTopLevelOnlyAndSkipsInternalRecursion() { // graph-structure subgraph compile, MultiAgentCompiler swarm) must go // through the non-merging entry so nested specialist sub-agents don't // silently pick up unrelated server-side tools. - WorkflowDef flagged = wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "Use when stuck."); - when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(flagged)); + AgentCompiler compiler = new AgentCompiler(new AutoExposedToolsMerger( + List.of(registeredAgent("_helper_agent", "helper_agent", "Use when stuck.")))); AgentConfig inner = AgentConfig.builder() .name("inner_specialist") @@ -197,64 +204,70 @@ void mergeRunsOnceAtTopLevelOnlyAndSkipsInternalRecursion() { } @Test - void daoQueriedOnlyOnceAcrossMultipleMerges() { - // Lazy cache: registered server-side agents are written at @PostConstruct - // and don't change at runtime, so the per-compile DAO fetch is wasted - // work after the first one. Pin the contract so anyone removing the - // cache trips this test. - WorkflowDef flagged = wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "x"); - when(metadataDAO.getAllWorkflowDefsLatestVersions()).thenReturn(List.of(flagged)); - - AgentConfig a = - AgentConfig.builder().name("agent_a").tools(new ArrayList<>()).build(); - AgentConfig b = - AgentConfig.builder().name("agent_b").tools(new ArrayList<>()).build(); - - compiler.mergeAutoExposedTools(a); - compiler.mergeAutoExposedTools(b); - - verify(metadataDAO, times(1)).getAllWorkflowDefsLatestVersions(); - // Both configs still received the merge from the cached result. - assertThat(a.getTools()).extracting(ToolConfig::getName).contains("helper_agent"); - assertThat(b.getTools()).extracting(ToolConfig::getName).contains("helper_agent"); + void agentConfigReadOnceAtConstructionNotPerMerge() { + // Entries are fixed when the merger is constructed from the bean + // list — per-merge re-reads would be wasted work (registered agents + // don't change at runtime) and would re-trigger any validation in + // the agent's config factory on every user compile. + AtomicInteger reads = new AtomicInteger(); + RegisteredAgent counting = new RegisteredAgent() { + @Override + public AgentConfig agentConfig() { + reads.incrementAndGet(); + return AgentConfig.builder().name("_helper_agent").build(); + } + + @Override + public ExposeAsTool autoExpose() { + return new ExposeAsTool("helper_agent", "x"); + } + }; + AutoExposedToolsMerger merger = new AutoExposedToolsMerger(List.of(counting)); + + merger.merge(AgentConfig.builder().name("a").tools(new ArrayList<>()).build()); + merger.merge(AgentConfig.builder().name("b").tools(new ArrayList<>()).build()); + + assertThat(reads).hasValue(1); } @Test - void daoFailureIsNotCachedAndIsRetriedOnNextMerge() { - // Caching the failure path would turn a transient blip into a - // permanent silent loss of the merge. Stub: first call throws, second - // call returns a flagged def. The second merge must pick it up. - when(metadataDAO.getAllWorkflowDefsLatestVersions()) - .thenThrow(new RuntimeException("transient DAO failure")) - .thenReturn(List.of(wfWithAutoExposeMetadata("_helper_agent", "helper_agent", "x"))); - - AgentConfig first = - AgentConfig.builder().name("first").tools(new ArrayList<>()).build(); - AgentConfig second = - AgentConfig.builder().name("second").tools(new ArrayList<>()).build(); - - compiler.mergeAutoExposedTools(first); - compiler.mergeAutoExposedTools(second); - - // First merge happened during the transient failure → no auto-expose. - assertThat(first.getTools()).extracting(ToolConfig::getName).doesNotContain("helper_agent"); - // Second merge re-queried the DAO and picked the entry up. - assertThat(second.getTools()).extracting(ToolConfig::getName).contains("helper_agent"); - verify(metadataDAO, times(2)).getAllWorkflowDefsLatestVersions(); + void blankToolNameFailsFastAtConstruction() { + // A blank LLM-facing tool name is a programming error in the + // RegisteredAgent bean — surfacing it at boot beats silently + // registering an unusable tool. + RegisteredAgent broken = new RegisteredAgent() { + @Override + public AgentConfig agentConfig() { + return AgentConfig.builder().name("_broken_agent").build(); + } + + @Override + public ExposeAsTool autoExpose() { + return new ExposeAsTool(" ", "description"); + } + }; + + assertThatThrownBy(() -> new AutoExposedToolsMerger(List.of(broken))) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("blank tool name"); } // ───────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────── - private static WorkflowDef wfWithAutoExposeMetadata(String workflowName, String toolName, String description) { - WorkflowDef def = new WorkflowDef(); - def.setName(workflowName); - Map metadata = new LinkedHashMap<>(); - metadata.put( - AgentCompiler.AUTO_EXPOSE_AS_TOOL_METADATA_KEY, Map.of("name", toolName, "description", description)); - def.setMetadata(metadata); - return def; + private static RegisteredAgent registeredAgent(String workflowName, String toolName, String description) { + return new RegisteredAgent() { + @Override + public AgentConfig agentConfig() { + return AgentConfig.builder().name(workflowName).build(); + } + + @Override + public ExposeAsTool autoExpose() { + return new ExposeAsTool(toolName, description); + } + }; } private static ToolConfig workerTool(String name) { diff --git a/server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java b/server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java index 5cb3f947b..5402b3817 100644 --- a/server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java +++ b/server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java @@ -9,7 +9,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.List; @@ -26,30 +25,22 @@ import dev.agentspan.runtime.registry.RegisteredAgentRegistrar; /** - * Bootstrap-ordering regression test for the {@link RegisteredAgentRegistrar} + * Bootstrap-ordering test for the {@link RegisteredAgentRegistrar} * + auto-expose merger interaction. * - *

    The merger lazily caches the DAO's auto-exposed-workflow list on first - * read and never refreshes it. If the registrar performs the cache-triggering - * read during its own bootstrap loop — i.e. before the agent it's - * registering has been written to the DAO — the cache snapshots an empty list - * and stays empty for the bean's lifetime. The OCG sub-agent (and every future - * server-registered agent) silently becomes invisible to user compiles.

    - * - *

    This test pins the contract by exercising the real registrar against a - * stateful in-memory DAO and verifying that, after bootstrap, a user-side - * merge picks up the agent the registrar just persisted.

    + *

    The merger reads its entries straight from the {@link RegisteredAgent} + * bean list at construction, so a user compile sees every registered agent + * regardless of whether the registrar's {@code @PostConstruct} DAO writes + * have happened yet. (The previous DAO-scan design could snapshot an empty + * cache during bootstrap and silently hide every registered agent for the + * bean's lifetime — this test pins that the trap is structurally gone.)

    */ class RegisteredAgentBootstrapTest { @Test - void userMergeAfterBootstrapSeesAgentsTheRegistrarJustWrote() { - // Stateful in-memory DAO — getAllWorkflowDefsLatestVersions reflects - // whatever has been written so far. Mirrors Conductor's real - // persistence behaviour during boot. + void userCompileSeesRegisteredAgentsRegardlessOfRegistrarOrdering() { MetadataDAO dao = mock(MetadataDAO.class); List daoState = new ArrayList<>(); - when(dao.getAllWorkflowDefsLatestVersions()).thenAnswer(inv -> List.copyOf(daoState)); doAnswer(inv -> { daoState.add(inv.getArgument(0)); return null; @@ -57,12 +48,6 @@ void userMergeAfterBootstrapSeesAgentsTheRegistrarJustWrote() { .when(dao) .updateWorkflowDef(any(WorkflowDef.class)); - AgentCompiler compiler = new AgentCompiler(dao); - - // Stub a registered agent whose compile path doesn't need a real - // model — the AgentConfig is built with no tools and no model so it - // routes through compileSimple (which only needs the name + builds a - // workflow). What matters here is the order: register → write → merge. RegisteredAgent helper = new RegisteredAgent() { @Override public AgentConfig agentConfig() { @@ -80,31 +65,37 @@ public ExposeAsTool autoExpose() { } }; - // Bootstrap path — this is where the cache-population bug fires - // pre-fix: the registrar's compile() call queries the (still-empty) - // DAO and caches an empty entry list. - new RegisteredAgentRegistrar(compiler, dao, List.of(helper)).registerAll(); + // Production wiring: the merger is built from the same bean list the + // registrar iterates. + AgentCompiler compiler = new AgentCompiler(new AutoExposedToolsMerger(List.of(helper))); - // Sanity: the registrar wrote the auto-expose-marked def to the DAO. - assertThat(daoState).hasSize(1); - assertThat(daoState.get(0).getMetadata()) - .as("registrar must stamp the auto-expose metadata key") - .containsKey(AgentCompiler.AUTO_EXPOSE_AS_TOOL_METADATA_KEY); - - // The actual contract under test: a user agent compiled AFTER bootstrap - // must see the registered agent in its tools list. Pre-fix this fails - // because the cache was populated empty during step 1, before the DAO - // actually had the registered def in it. - AgentConfig userAgent = AgentConfig.builder() - .name("user_agent") + // A compile BEFORE the registrar has written anything to the DAO — + // the exact window where the old DAO-scan design froze an empty + // cache — must already see the registered agent. + AgentConfig earlyAgent = AgentConfig.builder() + .name("early_agent") + .model("openai/gpt-4o-mini") .tools(new ArrayList<>()) .build(); - - compiler.mergeAutoExposedTools(userAgent); - - assertThat(userAgent.getTools()) + compiler.compile(earlyAgent); + assertThat(earlyAgent.getTools()) .extracting(ToolConfig::getName) - .as("first user merge after bootstrap must see the just-registered agent") + .as("compile before registrar bootstrap must already see the registered agent") .contains("helper_tool"); + + // Bootstrap: the registrar persists the WorkflowDef so SUB_WORKFLOW + // dispatch can resolve '_helper_agent' by name at runtime. + new RegisteredAgentRegistrar(compiler, dao, List.of(helper)).registerAll(); + assertThat(daoState).hasSize(1); + assertThat(daoState.get(0).getName()).isEqualTo("_helper_agent"); + + // And a compile after bootstrap sees it too, of course. + AgentConfig lateAgent = AgentConfig.builder() + .name("late_agent") + .model("openai/gpt-4o-mini") + .tools(new ArrayList<>()) + .build(); + compiler.compile(lateAgent); + assertThat(lateAgent.getTools()).extracting(ToolConfig::getName).contains("helper_tool"); } } diff --git a/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java b/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java index 3ec3315f8..f3e962c41 100644 --- a/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java +++ b/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java @@ -8,13 +8,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -30,8 +29,9 @@ * Unit tests for {@link RegisteredAgentRegistrar}. * *

    Pins the contract every server-side sub-agent (OCG and any future - * peer) relies on: declare a bean, the framework compiles it, stamps - * auto-expose metadata if requested, and writes it to the metadata DAO.

    + * peer) relies on: declare a bean, the framework compiles it and writes + * it to the metadata DAO so SUB_WORKFLOW dispatch resolves it by name. + * LLM visibility is the merger's job, not the registrar's.

    */ class RegisteredAgentRegistrarTest { @@ -46,7 +46,7 @@ void compilesAndRegistersEveryRegisteredAgent() { return def; }); - RegisteredAgent a = stubAgent("alpha_agent", null); + RegisteredAgent a = stubAgent("alpha_agent", new ExposeAsTool("alpha_tool", "first")); RegisteredAgent b = stubAgent("beta_agent", null); new RegisteredAgentRegistrar(compiler, dao, List.of(a, b)).registerAll(); @@ -54,54 +54,11 @@ void compilesAndRegistersEveryRegisteredAgent() { verify(compiler).compileWithoutAutoExpose(a.agentConfig()); verify(compiler).compileWithoutAutoExpose(b.agentConfig()); ArgumentCaptor captor = ArgumentCaptor.forClass(WorkflowDef.class); - verify(dao, org.mockito.Mockito.times(2)).updateWorkflowDef(captor.capture()); + verify(dao, times(2)).updateWorkflowDef(captor.capture()); assertThat(captor.getAllValues().stream().map(WorkflowDef::getName)) .containsExactlyInAnyOrder("alpha_agent", "beta_agent"); } - @Test - void stampsAutoExposeMetadataWhenAgentRequestsIt() { - // The stamp is what makes a registered agent LLM-visible to other - // agents via AgentCompiler.mergeAutoExposedTools. Drift here would - // silently hide every server-side sub-agent from end-user agents. - AgentCompiler compiler = mock(AgentCompiler.class); - MetadataDAO dao = mock(MetadataDAO.class); - when(compiler.compileWithoutAutoExpose(any())).thenReturn(emptyDef("helper")); - - RegisteredAgent agent = stubAgent("helper", new ExposeAsTool("helper_tool", "Call when stuck.")); - - new RegisteredAgentRegistrar(compiler, dao, List.of(agent)).registerAll(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(WorkflowDef.class); - verify(dao).updateWorkflowDef(captor.capture()); - Object stamped = captor.getValue().getMetadata().get(AgentCompiler.AUTO_EXPOSE_AS_TOOL_METADATA_KEY); - assertThat(stamped).isInstanceOf(Map.class); - @SuppressWarnings("unchecked") - Map spec = (Map) stamped; - assertThat(spec).containsEntry("name", "helper_tool").containsEntry("description", "Call when stuck."); - } - - @Test - void doesNotStampWhenAutoExposeReturnsNull() { - // A registered agent that exists server-side but isn't meant to be - // LLM-visible (private helper, internal pipeline) must come back - // from the DAO without the auto-expose flag. - AgentCompiler compiler = mock(AgentCompiler.class); - MetadataDAO dao = mock(MetadataDAO.class); - when(compiler.compileWithoutAutoExpose(any())).thenReturn(emptyDef("internal")); - - new RegisteredAgentRegistrar(compiler, dao, List.of(stubAgent("internal", null))).registerAll(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(WorkflowDef.class); - verify(dao).updateWorkflowDef(captor.capture()); - Map metadata = captor.getValue().getMetadata(); - // metadata may be null OR present without the auto-expose key — - // either way the LLM-visibility contract isn't tripped. - if (metadata != null) { - assertThat(metadata).doesNotContainKey(AgentCompiler.AUTO_EXPOSE_AS_TOOL_METADATA_KEY); - } - } - @Test void emptyAgentListIsANoOp() { AgentCompiler compiler = mock(AgentCompiler.class); @@ -135,11 +92,4 @@ public ExposeAsTool autoExpose() { } }; } - - private static WorkflowDef emptyDef(String name) { - WorkflowDef def = new WorkflowDef(); - def.setName(name); - def.setMetadata(new LinkedHashMap<>()); - return def; - } } From fd505f49e39472d8bbe5fe056aa8782a06deec7e Mon Sep 17 00:00:00 2001 From: nicholascole Date: Thu, 11 Jun 2026 11:01:11 -0700 Subject: [PATCH 23/61] fix(ocg): anchor the sub-agent's "today" at execution time, not boot time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OcgAgentFactory baked LocalDate.now() into the registered system prompt, so the date anchor for "recent" / relative-range queries was the server's boot date. On a long-running server the prompt drifts until it claims last week (or last month) is "today", and the LLM bounds every relative query against the wrong anchor — the exact hallucinated-date failure the anchor was added to prevent. Now the agent_tool dispatch script (which runs per execution) injects __today__ = current UTC date into every sub-workflow's input, and the OCG prompt references ${workflow.input.__today__}, substituted by Conductor when the LLM task is scheduled. The injection is generic: any future sub-agent prompt can use the same input. Tests written first and confirmed red before the fix, per CLAUDE.md: - OcgAgentFactoryTest pins the prompt to the runtime expression - EnrichToolsScriptTest executes the real dispatch script in GraalJS and asserts the SUB_WORKFLOW input carries a yyyy-MM-dd __today__ Co-Authored-By: Claude Fable 5 --- .../runtime/ocg/OcgAgentFactory.java | 28 ++++++++-------- .../runtime/util/JavaScriptBuilder.java | 14 ++++++-- .../runtime/ocg/OcgAgentFactoryTest.java | 17 +++++----- .../runtime/util/EnrichToolsScriptTest.java | 32 +++++++++++++++++-- 4 files changed, 65 insertions(+), 26 deletions(-) diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java index c61e7728a..29c9f8b79 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java @@ -5,8 +5,6 @@ package dev.agentspan.runtime.ocg; -import java.time.LocalDate; -import java.time.ZoneOffset; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -48,19 +46,23 @@ public final class OcgAgentFactory { + "supporting citations."; /** - * Marker {@link #build} replaces with the current UTC date so the LLM - * doesn't hallucinate date ranges. Computed at compile time, refreshes - * on every server restart. + * Conductor expression the system prompt uses as its date anchor. + * Resolved per execution against the {@code _ocg_agent} sub-workflow's + * input — the agent_tool dispatch script injects {@code __today__} with + * the current UTC date on every call. Anchoring at execution time (not + * boot time) is what keeps "recent" / relative-date queries correct on + * a long-running server; a date baked in at registration would drift + * until the prompt claimed last month was "today". */ - static final String TODAY_PLACEHOLDER = "{{TODAY}}"; + static final String TODAY_EXPRESSION = "${workflow.input.__today__}"; /** - * System prompt template for the OCG sub-agent. {@link #TODAY_PLACEHOLDER} - * is replaced with today's UTC date in {@link #build} so any - * "recent" / relative-date query gets bounded against a real anchor - * instead of whatever year the model felt like inventing. + * System prompt for the OCG sub-agent. {@link #TODAY_EXPRESSION} is + * substituted by Conductor when the LLM task is scheduled, so any + * "recent" / relative-date query gets bounded against the real current + * date instead of whatever year the model felt like inventing. */ - static final String OCG_SYSTEM_PROMPT = "Today's date is " + TODAY_PLACEHOLDER + " (UTC). When a user asks for\n" + static final String OCG_SYSTEM_PROMPT = "Today's date is " + TODAY_EXPRESSION + " (UTC). When a user asks for\n" + "\"recent\" / \"last week\" / any relative range, anchor on this date.\n" + "Never invent a date range — if no range is implied by the user, omit\n" + "start_time/end_time from the request.\n\n" @@ -109,13 +111,11 @@ public static AgentConfig build(OcgProperties props) { + "Set OCG_MODEL (or -Dagentspan.ocg.model=…) to the LLM the OCG sub-agent " + "should use, e.g. OCG_MODEL=openai/gpt-4o-mini."); } - String prompt = OCG_SYSTEM_PROMPT.replace( - TODAY_PLACEHOLDER, LocalDate.now(ZoneOffset.UTC).toString()); return AgentConfig.builder() .name(AGENT_NAME) .description("Retrieval sub-agent over the Open Context Graph (OCG).") .model(props.getModel()) - .instructions(prompt) + .instructions(OCG_SYSTEM_PROMPT) .tools(buildTools()) .maxTurns(10) .build(); diff --git a/server/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java b/server/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java index 1ad4554e0..997825f28 100644 --- a/server/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java +++ b/server/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java @@ -614,7 +614,12 @@ public static String enrichToolsScript( + " if (!_req) _req = JSON.stringify(_p);" + " t.inputParameters = {" + " prompt: _req," - + " session_id: $.session_id || ''};" + + " session_id: $.session_id || ''," + // Current UTC date, evaluated when this dispatch script runs — + // sub-agent prompts reference ${workflow.input.__today__} to + // anchor relative-date queries without drifting from a date + // computed at compile/boot time. + + " __today__: new Date().toISOString().slice(0, 10)};" + " if ($.agentspanCtx) { t.inputParameters.__agentspan_ctx__ = $.agentspanCtx; }" + " if (agentToolCfg[n].retryCount !== undefined) t.retryCount = agentToolCfg[n].retryCount;" + " if (agentToolCfg[n].retryDelaySeconds !== undefined) t.retryDelaySeconds = agentToolCfg[n].retryDelaySeconds;" @@ -1244,7 +1249,12 @@ public static String enrichToolsScriptDynamic( + " if (!_req) _req = JSON.stringify(_p);" + " t.inputParameters = {" + " prompt: _req," - + " session_id: $.session_id || ''};" + + " session_id: $.session_id || ''," + // Current UTC date, evaluated when this dispatch script runs — + // sub-agent prompts reference ${workflow.input.__today__} to + // anchor relative-date queries without drifting from a date + // computed at compile/boot time. + + " __today__: new Date().toISOString().slice(0, 10)};" + " if ($.agentspanCtx) { t.inputParameters.__agentspan_ctx__ = $.agentspanCtx; }" + " if (agentToolCfg[n].retryCount !== undefined) t.retryCount = agentToolCfg[n].retryCount;" + " if (agentToolCfg[n].retryDelaySeconds !== undefined) t.retryDelaySeconds = agentToolCfg[n].retryDelaySeconds;" diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java index 87a3f7ba4..bc84bc0c6 100644 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java @@ -8,8 +8,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import java.time.LocalDate; -import java.time.ZoneOffset; import java.util.List; import org.junit.jupiter.api.Test; @@ -75,13 +73,16 @@ void exposesAllSevenOcgToolsWithMatchingToolTypes() { } @Test - void systemPromptHasTodayUtcDateSubstituted() { - // The {{TODAY}} placeholder is the anchor for "recent" / "last week" - // style queries. If a refactor silently drops the .replace() call, the - // LLM gets a literal "{{TODAY}}" and starts inventing dates again — - // exactly the failure mode the placeholder was added to prevent. + void systemPromptReferencesRuntimeDateInput() { + // The date anchor for "recent" / "last week" style queries must be a + // Conductor expression resolved per execution from the __today__ + // sub-workflow input (supplied by the agent_tool dispatch script) — + // NOT a date baked in at boot, which drifts on a long-running server + // until the prompt claims yesterday (or last month) is "today". String prompt = OcgAgentFactory.build(props()).getInstructions().toString(); - assertThat(prompt).contains(LocalDate.now(ZoneOffset.UTC).toString()); + // "Today's date is " — anything else (a literal date, a + // leftover placeholder) means the anchor is frozen at boot time. + assertThat(prompt).contains("Today's date is ${workflow.input.__today__}"); assertThat(prompt).doesNotContain("{{TODAY}}"); } diff --git a/server/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java b/server/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java index 047316224..c3cc93e85 100644 --- a/server/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java +++ b/server/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java @@ -40,12 +40,17 @@ void tearDown() { graalCtx.close(); } - @SuppressWarnings("unchecked") private List> enrich(String knownNamesJson, String toolCallsJson) throws Exception { // All optional config maps are empty so every name falls through to the // generic SIMPLE-or-unknown branch. That's the path the harness uses. + return enrichWithAgentTools("{}", knownNamesJson, toolCallsJson); + } + + @SuppressWarnings("unchecked") + private List> enrichWithAgentTools( + String agentToolJson, String knownNamesJson, String toolCallsJson) throws Exception { String script = JavaScriptBuilder.enrichToolsScript( - "{}", "{}", "{}", "{}", "{}", "{}", "{}", "{}", "{}", knownNamesJson); + "{}", "{}", "{}", agentToolJson, "{}", "{}", "{}", "{}", "{}", knownNamesJson); // Wrap so the script's IIFE return is captured AND we get a JSON string // back — Graal's Value.toString() is JS source, not JSON. String wrapped = "var $ = {" @@ -162,6 +167,29 @@ void prefillToolAlsoInDeclaredToolsIsCallable() throws Exception { assertThat(tasks.get(0).get("name")).isEqualTo("contextbook_read"); } + @Test + void agentToolDispatchInjectsTodayDateInput() throws Exception { + // Sub-agents whose prompts anchor relative dates ("recent", "last + // week") reference ${workflow.input.__today__}. The dispatch script + // runs per execution, so the date it injects is the actual current + // date — unlike anything computed at compile/boot time, which + // drifts on a long-running server. + String agentTools = "{\"helper_agent\": {\"workflowName\": \"_helper_agent\"}}"; + String known = "{\"helper_agent\": true}"; + String toolCalls = "[{\"name\": \"helper_agent\", \"taskReferenceName\": \"c1\"," + + " \"inputParameters\": {\"request\": \"find recent alerts\"}}]"; + + List> tasks = enrichWithAgentTools(agentTools, known, toolCalls); + assertThat(tasks).hasSize(1); + Map t = tasks.get(0); + assertThat(t.get("type")).isEqualTo("SUB_WORKFLOW"); + @SuppressWarnings("unchecked") + Map ip = (Map) t.get("inputParameters"); + assertThat((String) ip.get("__today__")) + .as("dispatch must inject today's UTC date as __today__ sub-workflow input") + .matches("\\d{4}-\\d{2}-\\d{2}"); + } + @Test void mixedKnownAndUnknownInOneTurn() throws Exception { String known = "{\"shell\": true}"; From 2da2b4d1b5bb84fad11cd399c25f5a0e5b75b3b6 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Thu, 11 Jun 2026 11:19:12 -0700 Subject: [PATCH 24/61] fix(ocg): tell the model to omit end_time for ranges that extend to now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Observed in execution 24fd619d (2026-06-11): asked to "catch me up on the current state", the sub-agent issued an ocg_query with end_time=2026-06-01 — ten days before today — silently dropping the newest data. The __today__ runtime anchor was working (the substituted prompt read "Today's date is 2026-06-11"); the model still closed the window early because (a) the prompt's example showed a hardcoded month-shaped window ending before today, which the model imitated, and (b) nothing said what end_time should be for open-ended questions. Prompt changes: - Explicit rule: ranges extending to the present ("recent", "current state", "catch me up") set start_time and OMIT end_time; end_time is only for windows that closed in the past. - The example's literal dates are gone — replaced with a relative "" start and no end_time — so there are no stale calendar dates in the prompt for the model to anchor on. Test written first and confirmed red, per CLAUDE.md: pins the omit-rule text and asserts the prompt contains no literal yyyy-MM-dd dates. Co-Authored-By: Claude Fable 5 --- .../agentspan/runtime/ocg/OcgAgentFactory.java | 14 ++++++++++---- .../runtime/ocg/OcgAgentFactoryTest.java | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java index 29c9f8b79..d831c0149 100644 --- a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java +++ b/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java @@ -65,7 +65,12 @@ public final class OcgAgentFactory { static final String OCG_SYSTEM_PROMPT = "Today's date is " + TODAY_EXPRESSION + " (UTC). When a user asks for\n" + "\"recent\" / \"last week\" / any relative range, anchor on this date.\n" + "Never invent a date range — if no range is implied by the user, omit\n" - + "start_time/end_time from the request.\n\n" + + "start_time/end_time from the request.\n" + + "If the range extends to the present (\"recent\", \"current state\",\n" + + "\"catch me up\", \"last N days\"), set start_time and OMIT end_time —\n" + + "the results then run through now. Only set end_time when the user\n" + + "asks about a window that closed in the past. Never end a range at a\n" + + "month boundary before today; that silently drops the newest data.\n\n" + "You are querying an OCG (Observability Context Graph). It is a RETRIEVAL\n" + "engine over a knowledge graph of entities (messages, channels, people)\n" + "linked by claims and relationships. It is NOT an aggregation engine.\n\n" @@ -81,7 +86,8 @@ public final class OcgAgentFactory { + "For aggregation questions, use a TWO-STEP pattern:\n" + " 1. RETRIEVE: ask OCG for the raw set of relevant entities.\n" + " - Use specific terms (cluster names, error codes, channel names).\n" - + " - Use start_time / end_time in the request body to bound the range.\n" + + " - Use start_time (and end_time only for windows closed in the\n" + + " past) to bound the range.\n" + " - Set max_results high (e.g. 500) so you get the full set, not a\n" + " top-N sample.\n" + " - Avoid hedging words (\"frequently\", \"across\", \"occurrences\") —\n" @@ -96,9 +102,9 @@ public final class OcgAgentFactory { + "Good (step 1): {\n" + " \"query\": \"TIMED_OUT health check failure cluster\",\n" + " \"max_results\": 500,\n" - + " \"start_time\": \"2026-05-04T00:00:00Z\",\n" - + " \"end_time\": \"2026-06-04T00:00:00Z\"\n" + + " \"start_time\": \"T00:00:00Z\"\n" + "}\n" + + "(end_time omitted — the range runs through now.)\n" + "Then parse the returned citations, extract cluster names from titles,\n" + "build the frequency table in your reasoning."; diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java index bc84bc0c6..adaf21afd 100644 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java +++ b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java @@ -86,6 +86,21 @@ void systemPromptReferencesRuntimeDateInput() { assertThat(prompt).doesNotContain("{{TODAY}}"); } + @Test + void systemPromptTellsModelToOmitEndTimeForOpenEndedRanges() { + // Observed failure: asked to "catch me up on the current state", the + // model set end_time to a month boundary BEFORE today (2026-06-01 + // with today = 2026-06-11), silently dropping the most recent days. + // The prompt must instruct: a range that extends to the present has + // NO end_time. And it must not contain literal example dates — a + // hardcoded month-shaped window is exactly what the model imitated. + String prompt = OcgAgentFactory.build(props()).getInstructions().toString(); + assertThat(prompt).containsIgnoringCase("omit end_time"); + assertThat(prompt) + .as("no hardcoded yyyy-MM-dd example dates for the model to anchor on") + .doesNotContainPattern("\\d{4}-\\d{2}-\\d{2}"); + } + @Test void buildFailsFastWhenModelIsBlank() { OcgProperties noModel = new OcgProperties(); From bf9fcc744083b9c34956a4e6b839e70e5e48dde3 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Thu, 11 Jun 2026 14:34:11 -0700 Subject: [PATCH 25/61] Refactor files to fit new structure --- .../runtime/ocg/OcgAgentFactory.java | 0 .../agentspan/runtime/ocg/OcgProperties.java | 0 .../runtime/ocg/OcgRegisteredAgent.java | 0 .../runtime/ocg/OcgRegisteredTaskDefs.java | 0 .../agentspan/runtime/ocg/OcgRequestTask.java | 0 .../runtime/ocg/OcgRequestTaskConfig.java | 0 .../operation/OcgCodeHistoryOperation.java | 0 .../ocg/operation/OcgGetEntityOperation.java | 0 .../runtime/ocg/operation/OcgInputs.java | 0 .../operation/OcgMemoryDeleteOperation.java | 0 .../OcgMemoryReinforceOperation.java | 0 .../ocg/operation/OcgMemorySetOperation.java | 0 .../operation/OcgNeighborhoodOperation.java | 0 .../runtime/ocg/operation/OcgOperation.java | 0 .../ocg/operation/OcgQueryOperation.java | 0 .../runtime/ocg/operation/OcgRequest.java | 0 .../runtime/ocg/operation/OcgUri.java | 0 .../runtime/registry/RegisteredAgent.java | 0 .../registry/RegisteredAgentRegistrar.java | 0 .../runtime/registry/RegisteredTaskDefs.java | 0 .../registry/RegisteredTaskDefsRegistrar.java | 0 .../runtime/ocg/OcgAgentFactoryTest.java | 123 ------ .../runtime/ocg/OcgRequestTaskTest.java | 385 ------------------ .../ocg/OcgToolCompilerIntegrationTest.java | 52 --- .../RegisteredAgentRegistrarTest.java | 95 ----- .../RegisteredTaskDefsRegistrarTest.java | 61 --- 26 files changed, 716 deletions(-) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java (100%) delete mode 100644 server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java delete mode 100644 server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java delete mode 100644 server/src/test/java/dev/agentspan/runtime/ocg/OcgToolCompilerIntegrationTest.java delete mode 100644 server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java delete mode 100644 server/src/test/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrarTest.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java diff --git a/server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java diff --git a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java diff --git a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java diff --git a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java diff --git a/server/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java rename to server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java deleted file mode 100644 index adaf21afd..000000000 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgAgentFactoryTest.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.util.List; - -import org.junit.jupiter.api.Test; - -import dev.agentspan.runtime.model.AgentConfig; -import dev.agentspan.runtime.model.ToolConfig; - -/** - * Unit tests for {@link OcgAgentFactory}. - * - *

    Pins the agent's surface area — name, model wiring, tool count, tool - * name → toolType mapping — so a refactor cannot silently drop one of the - * seven OCG operations or rename an operation away from its registered - * {@code OCG_*} task type.

    - */ -class OcgAgentFactoryTest { - - private static OcgProperties props() { - OcgProperties p = new OcgProperties(); - p.setUrl("http://ocg.local"); - p.setModel("openai/gpt-4o-mini"); - return p; - } - - @Test - void builtAgentCarriesNameModelAndSystemPrompt() { - AgentConfig cfg = OcgAgentFactory.build(props()); - - assertThat(cfg.getName()).isEqualTo("_ocg_agent"); - assertThat(cfg.getModel()).isEqualTo("openai/gpt-4o-mini"); - // System prompt must include the retrieval/aggregation distinction so - // model behaviour stays aligned with the documented OCG contract. - assertThat(cfg.getInstructions().toString()) - .contains("RETRIEVAL") - .contains("aggregation") - .contains("TWO-STEP"); - } - - @Test - void exposesAllSevenOcgToolsWithMatchingToolTypes() { - List tools = OcgAgentFactory.build(props()).getTools(); - assertThat(tools).hasSize(7); - - // Each tool's `name` must match its `toolType` so ToolCompiler's - // TYPE_MAP lookup resolves to the right OCG_* task type. A drift here - // would silently route the tool call to a SIMPLE task with no worker. - for (ToolConfig t : tools) { - assertThat(t.getName()) - .as("tool name == toolType (so TYPE_MAP routes correctly)") - .isEqualTo(t.getToolType()); - } - - List names = tools.stream().map(ToolConfig::getName).toList(); - assertThat(names) - .containsExactlyInAnyOrder( - "ocg_query", - "ocg_get_entity", - "ocg_neighborhood", - "ocg_code_history", - "ocg_memory_set", - "ocg_memory_reinforce", - "ocg_memory_delete"); - } - - @Test - void systemPromptReferencesRuntimeDateInput() { - // The date anchor for "recent" / "last week" style queries must be a - // Conductor expression resolved per execution from the __today__ - // sub-workflow input (supplied by the agent_tool dispatch script) — - // NOT a date baked in at boot, which drifts on a long-running server - // until the prompt claims yesterday (or last month) is "today". - String prompt = OcgAgentFactory.build(props()).getInstructions().toString(); - // "Today's date is " — anything else (a literal date, a - // leftover placeholder) means the anchor is frozen at boot time. - assertThat(prompt).contains("Today's date is ${workflow.input.__today__}"); - assertThat(prompt).doesNotContain("{{TODAY}}"); - } - - @Test - void systemPromptTellsModelToOmitEndTimeForOpenEndedRanges() { - // Observed failure: asked to "catch me up on the current state", the - // model set end_time to a month boundary BEFORE today (2026-06-01 - // with today = 2026-06-11), silently dropping the most recent days. - // The prompt must instruct: a range that extends to the present has - // NO end_time. And it must not contain literal example dates — a - // hardcoded month-shaped window is exactly what the model imitated. - String prompt = OcgAgentFactory.build(props()).getInstructions().toString(); - assertThat(prompt).containsIgnoringCase("omit end_time"); - assertThat(prompt) - .as("no hardcoded yyyy-MM-dd example dates for the model to anchor on") - .doesNotContainPattern("\\d{4}-\\d{2}-\\d{2}"); - } - - @Test - void buildFailsFastWhenModelIsBlank() { - OcgProperties noModel = new OcgProperties(); - noModel.setUrl("http://ocg.local"); - // model deliberately unset — operator forgot OCG_MODEL. - assertThatThrownBy(() -> OcgAgentFactory.build(noModel)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("OCG_MODEL"); - } - - @Test - void queryToolDeclaresQueryAsRequiredInput() { - ToolConfig queryTool = OcgAgentFactory.build(props()).getTools().stream() - .filter(t -> "ocg_query".equals(t.getName())) - .findFirst() - .orElseThrow(); - Object required = queryTool.getInputSchema().get("required"); - assertThat(required).asList().contains("query"); - } -} diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java deleted file mode 100644 index 1cba269fb..000000000 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java +++ /dev/null @@ -1,385 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Flow; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.netflix.conductor.model.TaskModel; - -import dev.agentspan.runtime.ocg.operation.OcgGetEntityOperation; -import dev.agentspan.runtime.ocg.operation.OcgMemoryDeleteOperation; -import dev.agentspan.runtime.ocg.operation.OcgMemoryReinforceOperation; -import dev.agentspan.runtime.ocg.operation.OcgMemorySetOperation; -import dev.agentspan.runtime.ocg.operation.OcgNeighborhoodOperation; -import dev.agentspan.runtime.ocg.operation.OcgQueryOperation; - -/** - * Unit tests for {@link OcgRequestTask}. - * - *

    Exercises the thin orchestrator with each strategy plugged in to - * pin the per-endpoint URL/method contract, projection, capping, and - * error handling. End-to-end behaviour through the strategy is the - * surface most likely to drift when an operation is refactored.

    - */ -class OcgRequestTaskTest { - - private static OcgProperties props(String url) { - OcgProperties p = new OcgProperties(); - p.setUrl(url); - p.setResponseCapChars(8192); - return p; - } - - private static HttpResponse stub(int status, String body) { - @SuppressWarnings("unchecked") - HttpResponse resp = mock(HttpResponse.class); - when(resp.statusCode()).thenReturn(status); - when(resp.body()).thenReturn(body); - return resp; - } - - private static void stubSend(HttpClient http, HttpResponse response) throws Exception { - doReturn(response).when(http).send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)); - } - - private static TaskModel taskWith(Map input) { - TaskModel t = new TaskModel(); - t.setInputData(input); - return t; - } - - @Test - void queryOperationPostsToAgentQueryEndpoint() throws Exception { - HttpClient http = mock(HttpClient.class); - stubSend(http, stub(200, "{\"citations\":[{\"source_item_id\":\"a\",\"title\":\"t1\",\"snippet\":\"s\"}]}")); - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props("http://ocg.local"), http); - - TaskModel t = taskWith(Map.of("query", "find foo", "max_results", 50)); - task.start(null, t, null); - - ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - verify(http).send(req.capture(), any()); - assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/api/v1/agent/query"); - assertThat(req.getValue().method()).isEqualTo("POST"); - assertThat(t.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); - // Projection keeps citations[].source_item_id but JSON-serializes the - // whole result; the substring assertion pins both the projection and - // the serialized-into-``result`` shape downstream INLINEs read. - assertThat(t.getOutputData().get("result").toString()).contains("source_item_id"); - assertThat(t.getOutputData().get("operation")).isEqualTo("query"); - } - - @Test - void getEntityOperationGetsToEntitiesEndpoint() throws Exception { - HttpClient http = mock(HttpClient.class); - stubSend(http, stub(200, "{\"id\":\"e1\",\"type\":\"message\",\"title\":\"hello\"}")); - OcgRequestTask task = new OcgRequestTask(new OcgGetEntityOperation(), props("http://ocg.local/"), http); - - TaskModel t = taskWith(Map.of("entity_id", "e1")); - task.start(null, t, null); - - ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - verify(http).send(req.capture(), any()); - // Trailing slash on the configured URL is trimmed. - assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/api/v1/entities/e1"); - assertThat(req.getValue().method()).isEqualTo("GET"); - assertThat(t.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); - } - - @Test - void neighborhoodOperationIncludesDepthAndLimitQueryParams() throws Exception { - HttpClient http = mock(HttpClient.class); - stubSend(http, stub(200, "{\"center\":{\"id\":\"e1\"},\"edges\":[]}")); - OcgRequestTask task = new OcgRequestTask(new OcgNeighborhoodOperation(), props("http://ocg.local"), http); - - TaskModel t = taskWith(Map.of("entity_id", "e1", "depth", 1, "limit", 5)); - task.start(null, t, null); - - ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - verify(http).send(req.capture(), any()); - assertThat(req.getValue().uri().toString()) - .isEqualTo("http://ocg.local/api/v1/graph/neighborhood/e1?depth=1&limit=5"); - } - - @Test - void memoryDeleteOperationDispatchesDeleteWithQueryString() throws Exception { - HttpClient http = mock(HttpClient.class); - stubSend(http, stub(200, "{\"deleted\":true}")); - OcgRequestTask task = new OcgRequestTask(new OcgMemoryDeleteOperation(), props("http://ocg.local"), http); - - TaskModel t = taskWith(Map.of("key", "k1", "agent", "agent:foo", "user", "user:bar")); - task.start(null, t, null); - - ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - verify(http).send(req.capture(), any()); - assertThat(req.getValue().method()).isEqualTo("DELETE"); - // Spring's UriComponentsBuilder encodes only characters that RFC 3986 - // reserves for query values — ``:`` is not reserved there, so it - // stays raw. The previous hand-rolled URLEncoder.encode was over- - // encoding to ``%3A``; OCG accepts both, but the standards-compliant - // form is what the new builder produces. - assertThat(req.getValue().uri().toString()) - .isEqualTo("http://ocg.local/api/v1/memories/k1?agent=agent:foo&user=user:bar"); - } - - @Test - void non2xxResponseSurfacesAsFailedStatus() throws Exception { - HttpClient http = mock(HttpClient.class); - stubSend(http, stub(503, "service unavailable")); - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props("http://ocg.local"), http); - - TaskModel t = taskWith(Map.of("query", "x")); - task.start(null, t, null); - - assertThat(t.getStatus()).isEqualTo(TaskModel.Status.FAILED); - assertThat(t.getReasonForIncompletion()).contains("503"); - } - - @Test - void responseLargerThanCapIsTruncatedWithSuffix() throws Exception { - // Build a 50KB body so the post-projection JSON exceeds the 256-char - // cap configured below. The truncation suffix must be present and - // the result must not exceed the cap. - StringBuilder big = new StringBuilder("{\"citations\":["); - for (int i = 0; i < 200; i++) { - if (i > 0) big.append(','); - big.append("{\"source_item_id\":\"id") - .append(i) - .append("\",\"title\":\"title") - .append(i) - .append("\"}"); - } - big.append("]}"); - - HttpClient http = mock(HttpClient.class); - stubSend(http, stub(200, big.toString())); - OcgProperties p = props("http://ocg.local"); - p.setResponseCapChars(256); - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), p, http); - - TaskModel t = taskWith(Map.of("query", "x")); - task.start(null, t, null); - - String result = (String) t.getOutputData().get("result"); - assertThat(t.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); - assertThat(result).hasSizeLessThanOrEqualTo(256); - assertThat(result).endsWith("...[truncated]"); - } - - @Test - void authorizationHeaderAttachedWhenApiKeySet() throws Exception { - HttpClient http = mock(HttpClient.class); - stubSend(http, stub(200, "{\"citations\":[]}")); - OcgProperties p = props("http://ocg.local"); - p.setApiKey("secret-key-123"); - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), p, http); - - TaskModel t = taskWith(Map.of("query", "x")); - task.start(null, t, null); - - ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - verify(http).send(req.capture(), any()); - // Bearer scheme — pinning both the header name and the prefix so a - // refactor can't silently change to e.g. ``X-API-Key`` without - // tripping a test. - assertThat(req.getValue().headers().firstValue("Authorization")).hasValue("Bearer secret-key-123"); - } - - @Test - void noAuthorizationHeaderWhenApiKeyUnset() throws Exception { - HttpClient http = mock(HttpClient.class); - stubSend(http, stub(200, "{\"citations\":[]}")); - // props(...) does not set an api key → header must be omitted so - // unauthenticated local OCG instances keep working. - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props("http://ocg.local"), http); - - TaskModel t = taskWith(Map.of("query", "x")); - task.start(null, t, null); - - ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - verify(http).send(req.capture(), any()); - assertThat(req.getValue().headers().firstValue("Authorization")).isEmpty(); - } - - @Test - void disabledPropertiesYieldsFailedTask() throws Exception { - HttpClient http = mock(HttpClient.class); - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(""), http); - - TaskModel t = taskWith(Map.of("query", "x")); - task.start(null, t, null); - - assertThat(t.getStatus()).isEqualTo(TaskModel.Status.FAILED); - assertThat(t.getReasonForIncompletion()).contains("not configured"); - // Importantly: no HTTP call attempted when disabled. - verifyNoInteractions(http); - } - - @Test - void memorySetOperationPostsToMemoriesEndpointAndStripsAgentspanCtx() throws Exception { - HttpClient http = mock(HttpClient.class); - stubSend(http, stub(200, "{\"memory_id\":\"m1\"}")); - OcgRequestTask task = new OcgRequestTask(new OcgMemorySetOperation(), props("http://ocg.local"), http); - - Map input = new LinkedHashMap<>(); - input.put("key", "k1"); - input.put("agent", "agent:foo"); - input.put("user", "user:bar"); - input.put("string_value", "remember me"); - input.put("description", "test memory"); - // Server-side execution-token plumbing. Must NEVER be forwarded to OCG — - // it's an internal credential glob the agent framework rides on workflow - // inputs, not user-facing data. - input.put("__agentspan_ctx__", "execution-token-xyz"); - - TaskModel t = taskWith(input); - task.start(null, t, null); - - ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - verify(http).send(req.capture(), any()); - assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/api/v1/memories"); - assertThat(req.getValue().method()).isEqualTo("POST"); - - @SuppressWarnings("unchecked") - Map body = new ObjectMapper().readValue(bodyOf(req.getValue()), Map.class); - assertThat(body) - .containsEntry("key", "k1") - .containsEntry("agent", "agent:foo") - .containsEntry("user", "user:bar") - .containsEntry("string_value", "remember me") - .containsEntry("description", "test memory") - // The whole reason this test exists. - .doesNotContainKey("__agentspan_ctx__"); - assertThat(t.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); - } - - @Test - void memoryReinforceOperationPostsToReinforceEndpointWithFilteredBody() throws Exception { - HttpClient http = mock(HttpClient.class); - stubSend(http, stub(200, "{\"reinforced\":true}")); - OcgRequestTask task = new OcgRequestTask(new OcgMemoryReinforceOperation(), props("http://ocg.local"), http); - - Map input = new LinkedHashMap<>(); - input.put("key", "k1"); - input.put("agent", "agent:foo"); - input.put("user", "user:bar"); - input.put("confidence_boost", 0.05); - input.put("source_ref", "msg:42"); - // Must NOT leak. - input.put("__agentspan_ctx__", "execution-token-xyz"); - // Must NOT be projected into the body either — pick() only takes the - // explicitly listed fields. - input.put("rogue_field", "nope"); - - TaskModel t = taskWith(input); - task.start(null, t, null); - - ArgumentCaptor req = ArgumentCaptor.forClass(HttpRequest.class); - verify(http).send(req.capture(), any()); - // key is part of the URL; the body must be the picked subset. - assertThat(req.getValue().uri().toString()).isEqualTo("http://ocg.local/api/v1/memories/k1/reinforce"); - assertThat(req.getValue().method()).isEqualTo("POST"); - - @SuppressWarnings("unchecked") - Map body = new ObjectMapper().readValue(bodyOf(req.getValue()), Map.class); - // Allow-list: only the four picked fields are forwarded. Pinning the - // exact key set so a future widening of pick() can't silently leak - // server-side plumbing. - assertThat(body).containsOnlyKeys("agent", "user", "confidence_boost", "source_ref"); - assertThat(body) - .containsEntry("agent", "agent:foo") - .containsEntry("user", "user:bar") - .containsEntry("source_ref", "msg:42"); - } - - @Test - void interruptDuringHttpSendRestoresInterruptFlag() throws Exception { - HttpClient http = mock(HttpClient.class); - // HttpClient.send declares ``throws IOException, InterruptedException`` — - // doThrow on the checked InterruptedException is the canonical mocking path. - doThrow(new InterruptedException("cancelled")) - .when(http) - .send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)); - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props("http://ocg.local"), http); - - TaskModel t = taskWith(Map.of("query", "x")); - // Clear any flag accidentally left on by an earlier test on the shared - // test thread; we want to observe ONLY the flag the task itself sets. - Thread.interrupted(); - - task.start(null, t, null); - - // ``Thread.interrupted()`` both reads AND clears the flag — perfect for - // a one-shot assertion. The contract under test: catching - // InterruptedException must re-flag the current thread so Conductor's - // executor (and anyone else up the stack) can observe the cancellation. - boolean restored = Thread.interrupted(); - assertThat(restored) - .as("InterruptedException must be re-flagged on the current thread") - .isTrue(); - assertThat(t.getStatus()).isEqualTo(TaskModel.Status.FAILED); - assertThat(t.getReasonForIncompletion()).containsIgnoringCase("interrupt"); - } - - /** - * Drain an {@link HttpRequest}'s body publisher to a String. The JDK's - * {@code BodyPublishers.ofString} delivers synchronously on a single - * {@code onNext}, but we still complete a {@link CompletableFuture} on - * {@code onComplete} and wait briefly so the helper is safe against any - * future publisher variant. - */ - private static String bodyOf(HttpRequest req) throws Exception { - if (req.bodyPublisher().isEmpty()) return ""; - HttpRequest.BodyPublisher pub = req.bodyPublisher().get(); - CompletableFuture done = new CompletableFuture<>(); - pub.subscribe(new Flow.Subscriber<>() { - final StringBuilder sb = new StringBuilder(); - - @Override - public void onSubscribe(Flow.Subscription s) { - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(ByteBuffer item) { - sb.append(StandardCharsets.UTF_8.decode(item)); - } - - @Override - public void onError(Throwable t) { - done.completeExceptionally(t); - } - - @Override - public void onComplete() { - done.complete(sb.toString()); - } - }); - return done.get(5, TimeUnit.SECONDS); - } -} diff --git a/server/src/test/java/dev/agentspan/runtime/ocg/OcgToolCompilerIntegrationTest.java b/server/src/test/java/dev/agentspan/runtime/ocg/OcgToolCompilerIntegrationTest.java deleted file mode 100644 index dc4826780..000000000 --- a/server/src/test/java/dev/agentspan/runtime/ocg/OcgToolCompilerIntegrationTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import dev.agentspan.runtime.compiler.ToolCompiler; -import dev.agentspan.runtime.model.ToolConfig; - -/** - * Integration test pinning the OCG tool type → Conductor task type contract - * between {@link OcgAgentFactory} and {@link ToolCompiler}. - * - *

    If anyone ever renames an OCG tool type without updating ToolCompiler's - * TYPE_MAP, the LLM tool call would fall through to a SIMPLE task with no - * worker and the workflow would hang forever. This test catches that drift - * at compile time on the test surface.

    - */ -class OcgToolCompilerIntegrationTest { - - @Test - void everyOcgToolSpecCompilesToItsRegisteredSystemTaskType() { - ToolCompiler compiler = new ToolCompiler(); - List tools = OcgAgentFactory.buildTools(); - List> specs = compiler.compileToolSpecs(tools); - - Map expectedTaskTypes = Map.of( - "ocg_query", "OCG_QUERY", - "ocg_get_entity", "OCG_GET_ENTITY", - "ocg_neighborhood", "OCG_NEIGHBORHOOD", - "ocg_code_history", "OCG_CODE_HISTORY", - "ocg_memory_set", "OCG_MEMORY_SET", - "ocg_memory_reinforce", "OCG_MEMORY_REINFORCE", - "ocg_memory_delete", "OCG_MEMORY_DELETE"); - - for (Map spec : specs) { - String name = (String) spec.get("name"); - String conductorType = (String) spec.get("type"); - assertThat(conductorType) - .as("OCG tool '%s' must compile to its registered task type", name) - .isEqualTo(expectedTaskTypes.get(name)); - } - } -} diff --git a/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java b/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java deleted file mode 100644 index f3e962c41..000000000 --- a/server/src/test/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrarTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.registry; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import com.netflix.conductor.common.metadata.workflow.WorkflowDef; -import com.netflix.conductor.dao.MetadataDAO; - -import dev.agentspan.runtime.compiler.AgentCompiler; -import dev.agentspan.runtime.model.AgentConfig; -import dev.agentspan.runtime.registry.RegisteredAgent.ExposeAsTool; - -/** - * Unit tests for {@link RegisteredAgentRegistrar}. - * - *

    Pins the contract every server-side sub-agent (OCG and any future - * peer) relies on: declare a bean, the framework compiles it and writes - * it to the metadata DAO so SUB_WORKFLOW dispatch resolves it by name. - * LLM visibility is the merger's job, not the registrar's.

    - */ -class RegisteredAgentRegistrarTest { - - @Test - void compilesAndRegistersEveryRegisteredAgent() { - AgentCompiler compiler = mock(AgentCompiler.class); - MetadataDAO dao = mock(MetadataDAO.class); - when(compiler.compileWithoutAutoExpose(any())).thenAnswer(inv -> { - AgentConfig cfg = inv.getArgument(0); - WorkflowDef def = new WorkflowDef(); - def.setName(cfg.getName()); - return def; - }); - - RegisteredAgent a = stubAgent("alpha_agent", new ExposeAsTool("alpha_tool", "first")); - RegisteredAgent b = stubAgent("beta_agent", null); - - new RegisteredAgentRegistrar(compiler, dao, List.of(a, b)).registerAll(); - - verify(compiler).compileWithoutAutoExpose(a.agentConfig()); - verify(compiler).compileWithoutAutoExpose(b.agentConfig()); - ArgumentCaptor captor = ArgumentCaptor.forClass(WorkflowDef.class); - verify(dao, times(2)).updateWorkflowDef(captor.capture()); - assertThat(captor.getAllValues().stream().map(WorkflowDef::getName)) - .containsExactlyInAnyOrder("alpha_agent", "beta_agent"); - } - - @Test - void emptyAgentListIsANoOp() { - AgentCompiler compiler = mock(AgentCompiler.class); - MetadataDAO dao = mock(MetadataDAO.class); - - new RegisteredAgentRegistrar(compiler, dao, null).registerAll(); - new RegisteredAgentRegistrar(compiler, dao, List.of()).registerAll(); - - // Neither compile nor write should fire — the registrar must be - // benign when no RegisteredAgent beans are present (e.g. OCG off - // and no future sub-agents declared). - verifyNoInteractions(compiler); - verifyNoInteractions(dao); - } - - // ───────────────────────────────────────────────────────────────────── - // Helpers - // ───────────────────────────────────────────────────────────────────── - - private static RegisteredAgent stubAgent(String name, ExposeAsTool expose) { - AgentConfig config = AgentConfig.builder().name(name).build(); - return new RegisteredAgent() { - @Override - public AgentConfig agentConfig() { - return config; - } - - @Override - public ExposeAsTool autoExpose() { - return expose; - } - }; - } -} diff --git a/server/src/test/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrarTest.java b/server/src/test/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrarTest.java deleted file mode 100644 index b74e06e94..000000000 --- a/server/src/test/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrarTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.registry; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import com.netflix.conductor.common.metadata.tasks.TaskDef; -import com.netflix.conductor.dao.MetadataDAO; - -/** - * Unit tests for {@link RegisteredTaskDefsRegistrar}. - * - *

    Each {@link RegisteredTaskDefs} bean's contribution must reach the - * metadata DAO unmodified. Conductor's dynamic-dispatch lookup is by - * task name; missing or mangled defs surface as - * "Cannot find task by name X" at runtime.

    - */ -class RegisteredTaskDefsRegistrarTest { - - @Test - void writesEveryContributedTaskDefToTheDao() { - MetadataDAO dao = mock(MetadataDAO.class); - RegisteredTaskDefs supplierA = () -> List.of(def("ocg_query"), def("ocg_get_entity")); - RegisteredTaskDefs supplierB = () -> List.of(def("another_tool")); - - new RegisteredTaskDefsRegistrar(dao, List.of(supplierA, supplierB)).registerAll(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(TaskDef.class); - verify(dao, times(3)).updateTaskDef(captor.capture()); - assertThat(captor.getAllValues().stream().map(TaskDef::getName)) - .containsExactlyInAnyOrder("ocg_query", "ocg_get_entity", "another_tool"); - } - - @Test - void noSuppliersMeansNoWrites() { - MetadataDAO dao = mock(MetadataDAO.class); - - new RegisteredTaskDefsRegistrar(dao, null).registerAll(); - new RegisteredTaskDefsRegistrar(dao, List.of()).registerAll(); - - verifyNoInteractions(dao); - } - - private static TaskDef def(String name) { - TaskDef d = new TaskDef(); - d.setName(name); - return d; - } -} From 79752d31178b287f31eee2cc82362651e17e9d95 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Thu, 11 Jun 2026 15:57:57 -0700 Subject: [PATCH 26/61] dummy --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9133d2892..b3c043ae6 100644 --- a/README.md +++ b/README.md @@ -730,4 +730,4 @@ See [API Reference](docs/python-sdk/api-reference.md) for the complete API refer ## License -[MIT](LICENSE) +[MIT](LICENSE) \ No newline at end of file From d0f8c7bf7698f03fcdec6caebe6c6e1a4b5fe129 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Thu, 11 Jun 2026 17:21:30 -0700 Subject: [PATCH 27/61] Make changes compatible with core conductor --- .../compiler/AutoExposedToolsMerger.java | 19 +++++++++ .../registry/RegisteredTaskDefsRegistrar.java | 39 +++++++++++++++---- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java index c8ce483ad..d5aa10dbd 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -122,10 +123,28 @@ private static Set collectToolNames(AgentConfig config) { } private static ToolConfig buildAgentTool(String workflowName, ExposeAsTool expose) { + // Same schema OpenAINormalizer builds for user-declared agent tools — every + // agentspan tool is self-describing on the wire, so any host executor + // (standalone or embedded) can hand it to the LLM without type knowledge. + Map inputSchema = new LinkedHashMap<>(); + inputSchema.put("type", "object"); + inputSchema.put( + "properties", + Map.of( + "request", + Map.of( + "type", + "string", + "description", + "The request or question to send to this agent"))); + inputSchema.put("required", List.of("request")); + inputSchema.put("additionalProperties", false); + return ToolConfig.builder() .name(expose.toolName()) .toolType("agent_tool") .description(expose.toolDescription() != null ? expose.toolDescription() : "") + .inputSchema(inputSchema) .config(Map.of("workflowName", workflowName)) .build(); } diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java index 58a336eb9..9cbe97d4f 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java @@ -5,7 +5,10 @@ package dev.agentspan.runtime.registry; +import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import jakarta.annotation.PostConstruct; @@ -15,7 +18,7 @@ import org.springframework.stereotype.Component; import com.netflix.conductor.common.metadata.tasks.TaskDef; -import com.netflix.conductor.dao.MetadataDAO; +import com.netflix.conductor.service.MetadataService; /** * Generic registrar that writes every {@link RegisteredTaskDefs}-contributed @@ -31,25 +34,47 @@ public class RegisteredTaskDefsRegistrar { private static final Logger log = LoggerFactory.getLogger(RegisteredTaskDefsRegistrar.class); - private final MetadataDAO metadataDAO; + // Service layer, not MetadataDAO: orkes' fork changed the DAO's updateTaskDef + // return type (TaskDef -> String), so DAO calls compiled against OSS break at + // runtime when embedded. MetadataService.updateTaskDef is void in both. + private final MetadataService metadataService; private final List suppliers; @Autowired public RegisteredTaskDefsRegistrar( - MetadataDAO metadataDAO, @Autowired(required = false) List suppliers) { - this.metadataDAO = metadataDAO; + MetadataService metadataService, @Autowired(required = false) List suppliers) { + this.metadataService = metadataService; this.suppliers = suppliers != null ? suppliers : List.of(); } @PostConstruct public void registerAll() { - int count = 0; + if (suppliers.isEmpty()) { + return; + } + // Upsert via the service layer's create/update split: hosts differ on + // updateTaskDef semantics (OSS DAO upserts; orkes' service throws NOT_FOUND + // for unknown names), so check existence against the full list first. + Set existing = + metadataService.getTaskDefs().stream() + .map(TaskDef::getName) + .collect(Collectors.toSet()); + List toCreate = new ArrayList<>(); + int updated = 0; for (RegisteredTaskDefs supplier : suppliers) { for (TaskDef def : supplier.taskDefs()) { - metadataDAO.updateTaskDef(def); - count++; + if (existing.contains(def.getName())) { + metadataService.updateTaskDef(def); + updated++; + } else { + toCreate.add(def); + } } } + if (!toCreate.isEmpty()) { + metadataService.registerTaskDef(toCreate); + } + int count = updated + toCreate.size(); if (count > 0) { log.info("Registered {} TaskDef(s) from {} supplier(s)", count, suppliers.size()); } From 67c005f348b86f53770875e5037b61ed8adb27ab Mon Sep 17 00:00:00 2001 From: nicholascole Date: Fri, 12 Jun 2026 13:00:12 -0700 Subject: [PATCH 28/61] OCG as SDK-declared sub-agent: per-instance binding, auto-expose removed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OCG retrieval agent is now declared in user code via the Python SDK and every OCG tool binds the instance it talks to — there is no server-side OCG instance configuration of any kind. SDK (new module agentspan/agents/ocg.py): - ocg_agent(model=, url=, credential=, ...) — prebuilt retrieval Agent (canned prompt + 7 ocg_* tools, ported verbatim from OcgAgentFactory); wrap with agent_tool() to delegate retrieval. - ocg_tools(url=, credential=, ...) — raw ToolDefs with subset switches for custom retrieval agents. url is required on both; credential names a secrets-store entry, resolved server-side — the token never appears in Python code or workflow definitions. - Examples 116 (sub-agent) / 117 (direct tools) with shell run blocks; jira_ocg_smoke.py updated to the explicit opt-in shape. Server: - Deleted auto-expose (AutoExposedToolsMerger) and the registered-agent machinery (RegisteredAgent, RegisteredAgentRegistrar, OcgRegisteredAgent, OcgAgentFactory): an agent's compiled tool list is exactly its declared tool list. AgentCompiler.compile() is the single entry point. - Per-call instance resolution in OcgRequestTask via reserved __ocg_url/__ocg_auth inputs (compiled by ToolCompiler + enrich script); #{NAME} placeholders resolved through the credential store scoped by the execution token (OcgCredentialResolver/PlaceholderCredentialResolver); reserved inputs stripped before operations see the input map. - OcgProperties reduced to enabled + responseCapChars; OCG_MODEL/OCG_URL/ OCG_API_KEY removed. Gating moved to agentspan.ocg.enabled. - OcgToolValidator: agent starts with unbound or disabled OCG tools are rejected at start, recursing into sub-agents and inline agent_tool children. - selfDescribing marker stamped on every compiled tool spec — top-level (future ToolSpec field) and in configParams, the copy that survives ToolSpec deserialization; consumed by embedding hosts (OrkesLLM) to skip integration-store resolution. Verified live. - Fix: OCG TaskDefs now registered under the dispatched names (ocg_query, ...) — previously the operation labels (query, ...), which failed every dynamic-fork dispatch with "Cannot find task by name". Validation: full server suite green (35 new/updated OCG tests, fail-first per CLAUDE.md); SDK unit tests 17/17 + serialization wire-format test; e2e suite22 (two stub OCG instances) proves US/Canada traffic isolation on recorded HTTP traffic; smoke verified live against embedded orkes on 8080 with credential-by-name through the orkes secrets store. Docs: ocg-agent-flow.md rewritten, API reference section, release note, design doc + status tracker + orkes/OrkesLLM handoff plan under docs/design/. Co-Authored-By: Claude Fable 5 --- .gitignore | 3 + .../2026-06-12-ocg-sdk-subagent-design.md | 497 ++++++++++++++++++ .../2026-06-12-ocg-sdk-subagent-status.md | 181 +++++++ ...2026-06-12-toolspec-selfdescribing-plan.md | 143 +++++ docs/index.md | 2 +- docs/ocg-agent-flow.md | 443 ++++++---------- docs/python-sdk/api-reference.md | 65 +++ .../2026-06-12-ocg-sdk-subagent.md | 71 +++ e2e/ocg/jira_ocg_smoke.py | 60 +++ e2e/ocg/requirements.txt | 5 + sdk/python/e2e/test_suite22_ocg.py | 200 +++++++ sdk/python/examples/116_ocg_subagent.py | 88 ++++ sdk/python/examples/117_ocg_direct_tools.py | 88 ++++ sdk/python/src/agentspan/agents/__init__.py | 7 + sdk/python/src/agentspan/agents/ocg.py | 395 ++++++++++++++ sdk/python/tests/unit/test_ocg.py | 180 +++++++ .../src/main/resources/application.properties | 14 +- .../runtime/compiler/AgentCompilerTest.java | 26 + .../compiler/AutoExposedToolsMergeTest.java | 276 ---------- .../RegisteredAgentBootstrapTest.java | 101 ---- .../runtime/compiler/ToolCompilerTest.java | 111 ++++ .../ocg/OcgRegisteredTaskDefsTest.java | 36 ++ .../runtime/ocg/OcgRequestTaskTest.java | 204 +++++++ .../runtime/ocg/OcgToolValidatorTest.java | 116 ++++ .../runtime/compiler/AgentCompiler.java | 74 +-- .../compiler/AutoExposedToolsMerger.java | 157 ------ .../runtime/compiler/MultiAgentCompiler.java | 4 +- .../runtime/compiler/ToolCompiler.java | 42 +- .../runtime/ocg/OcgAgentFactory.java | 292 ---------- .../runtime/ocg/OcgCredentialResolver.java | 32 ++ .../agentspan/runtime/ocg/OcgProperties.java | 40 +- .../runtime/ocg/OcgRegisteredAgent.java | 49 -- .../runtime/ocg/OcgRegisteredTaskDefs.java | 25 +- .../agentspan/runtime/ocg/OcgRequestTask.java | 91 +++- .../runtime/ocg/OcgRequestTaskConfig.java | 72 ++- .../runtime/ocg/OcgToolValidator.java | 141 +++++ .../ocg/PlaceholderCredentialResolver.java | 78 +++ .../operation/OcgCodeHistoryOperation.java | 8 +- .../ocg/operation/OcgGetEntityOperation.java | 12 +- .../operation/OcgMemoryDeleteOperation.java | 8 +- .../OcgMemoryReinforceOperation.java | 8 +- .../ocg/operation/OcgMemorySetOperation.java | 8 +- .../operation/OcgNeighborhoodOperation.java | 8 +- .../runtime/ocg/operation/OcgOperation.java | 4 +- .../ocg/operation/OcgQueryOperation.java | 9 +- .../runtime/ocg/operation/OcgRequest.java | 20 +- .../runtime/ocg/operation/OcgTarget.java | 28 + .../runtime/ocg/operation/OcgUri.java | 6 +- .../runtime/registry/RegisteredAgent.java | 52 -- .../registry/RegisteredAgentRegistrar.java | 82 --- .../registry/RegisteredTaskDefsRegistrar.java | 9 +- .../runtime/service/AgentService.java | 16 + .../runtime/util/JavaScriptBuilder.java | 11 +- 53 files changed, 3174 insertions(+), 1524 deletions(-) create mode 100644 docs/design/2026-06-12-ocg-sdk-subagent-design.md create mode 100644 docs/design/2026-06-12-ocg-sdk-subagent-status.md create mode 100644 docs/design/2026-06-12-toolspec-selfdescribing-plan.md create mode 100644 docs/release-notes/2026-06-12-ocg-sdk-subagent.md create mode 100644 e2e/ocg/jira_ocg_smoke.py create mode 100644 e2e/ocg/requirements.txt create mode 100644 sdk/python/e2e/test_suite22_ocg.py create mode 100644 sdk/python/examples/116_ocg_subagent.py create mode 100644 sdk/python/examples/117_ocg_direct_tools.py create mode 100644 sdk/python/src/agentspan/agents/ocg.py create mode 100644 sdk/python/tests/unit/test_ocg.py delete mode 100644 server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java delete mode 100644 server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java create mode 100644 server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefsTest.java create mode 100644 server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java create mode 100644 server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgToolValidatorTest.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgCredentialResolver.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgToolValidator.java create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/PlaceholderCredentialResolver.java create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgTarget.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java diff --git a/.gitignore b/.gitignore index 03dcb169a..355f69015 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# OCG smoke test local venv +/e2e/ocg/venv/ + .DS_Store .idea/ __pycache__/ diff --git a/docs/design/2026-06-12-ocg-sdk-subagent-design.md b/docs/design/2026-06-12-ocg-sdk-subagent-design.md new file mode 100644 index 000000000..c734376b0 --- /dev/null +++ b/docs/design/2026-06-12-ocg-sdk-subagent-design.md @@ -0,0 +1,497 @@ +# OCG Sub-Agent via SDK — Fully SDK-Defined, Multi-Instance Design + +**Date:** 2026-06-12 +**Status:** Draft +**Supersedes:** the auto-expose mechanism and server-registered `_ocg_agent` described in `docs/ocg-agent-flow.md` + +--- + +## Overview + +Move the OCG sub-agent out of the server entirely and into the Python SDK: + +- **The SDK owns the agent definition** — system prompt, model, tool schemas, + turn limit — via an `ocg_agent()` factory that returns an ordinary `Agent`, + which users pass to their main agent with the existing `agent_tool()`. +- **The SDK owns the instance binding** — each OCG tool carries the target + OCG base URL and a **credential-store reference** (never the key itself), + so different agents can point at different OCG instances (multi-tenancy, + data residency: a US agent → US graph, a Canada agent → Canada graph). +- **The server owns execution** — the seven `OCG_*` system tasks keep doing + the HTTP calls, secret resolution, field projection, and response capping. + +Deleted outright: auto-expose (`AutoExposedToolsMerger`, +`RegisteredAgent.autoExpose()`, `ExposeAsTool`), the server-registered +`_ocg_agent` workflow, `OcgRegisteredAgent`, `OcgAgentFactory`, and — since +OCG was its only consumer — the `RegisteredAgent` registry +(`RegisteredAgentRegistrar`, `RegisteredAgent`). + +## Motivation + +- **Explicitness.** Tool injection that never appears in user code is + surprising in review and hard to test. After this change, an agent's tool + list in Python is its complete tool list. +- **Multi-instance / multi-tenancy.** A server-wide `OCG_URL` is structurally + single-instance: no agent definition, wherever it lives, can target a + second graph. Per-tool instance binding is the only shape that supports + US/Canada-style sharding and tenant-owned OCG instances. +- **One source of truth.** The previous two-tier design copied the tool + description and schemas from `OcgAgentFactory` into the SDK. With the + server-side definition deleted, the SDK copy *is* the definition. The + "tiers" collapse: customization is just keyword arguments on `ocg_agent()`. +- **Smaller footprint inside orkes-conductor.** AgentSpan is being embedded + into orkes-conductor as a single app. After this change AgentSpan registers + no workflows at boot, mutates no agents, and needs no `OCG_MODEL` — the + host integration shrinks to the task defs plus two *optional* default env + vars (§5). + +--- + +## 1. SDK API + +New module: `sdk/python/src/agentspan/agents/ocg.py`, exported from +`agentspan.agents`. + +### `ocg_agent()` — prebuilt retrieval agent + +```python +from agentspan.agents import Agent, agent_tool +from agentspan.agents.ocg import ocg_agent + +us_retriever = ocg_agent( + name="ocg_us", + url="https://us.ocg.example.com", + credential="OCG_US_KEY", # credential-store name, never the key + model="openai/gpt-4o-mini", +) +ca_retriever = ocg_agent( + name="ocg_canada", + url="https://ca.ocg.example.com", + credential="OCG_CA_KEY", +) + +us_agent = Agent(name="us_support", model="openai/gpt-4o", + tools=[agent_tool(us_retriever)], instructions="...") +ca_agent = Agent(name="ca_support", model="openai/gpt-4o", + tools=[agent_tool(ca_retriever)], instructions="...") + +# Or: one agent that routes by geography — impossible under auto-expose +router = Agent( + name="na_support", model="openai/gpt-4o", + tools=[ + agent_tool(us_retriever, description="Retrieve context for US customers"), + agent_tool(ca_retriever, description="Retrieve context for Canadian customers"), + ], + instructions="...", +) +``` + +```python +def ocg_agent( + *, + name: str = "ocg_agent", + model: str, # required — no silent default (mirrors the old OCG_MODEL fail-fast) + url: str, # required — every tool binds its own instance + credential: Optional[str] = None, # credential-store name for the OCG bearer token + instructions: Optional[str] = None, # defaults to the canned OCG system prompt + max_turns: int = 10, + # tool-subset switches, forwarded to ocg_tools(): + query: bool = True, + entities: bool = True, # ocg_get_entity + ocg_neighborhood + code_history: bool = True, + memory: bool = True, # ocg_memory_set / _reinforce / _delete +) -> Agent: + return Agent( + name=name, + model=model, + instructions=instructions or OCG_SYSTEM_PROMPT, + tools=ocg_tools(url=url, credential=credential, query=query, + entities=entities, code_history=code_history, memory=memory), + max_turns=max_turns, + ) +``` + +The canned `OCG_SYSTEM_PROMPT` and per-tool schemas move verbatim from +`OcgAgentFactory.java` into `ocg.py` — including the behavioral fixes already +shipped there (execution-time "today" anchoring, omit-`end_time`-for-open-ranges +guidance). The SDK becomes their only home. + +**Multi-instance naming requirement:** agents pointing at different OCG +instances MUST have distinct `name`s. Child `agent_tool` workflows are +registered by agent name (`AgentService.registerAgentToolWorkflows()` → +`updateWorkflowDef`), so two differently-configured agents both named +`ocg_agent` would overwrite each other's workflow definition. `ocg_agent()` +docstring carries this warning. (This is a general property of inline agent +tools, not OCG-specific.) + +### `ocg_tools()` — raw tools for custom retrieval agents + +```python +def ocg_tools( + *, + url: Optional[str] = None, + credential: Optional[str] = None, + query: bool = True, + entities: bool = True, + code_history: bool = True, + memory: bool = True, +) -> List[ToolDef]: ... +``` + +Returns up to seven `ToolDef`s with `tool_type="ocg_query"` … +`"ocg_memory_delete"` and the canonical input schemas. For users who want +their own prompt/model/composition and attach OCG tools to an agent they +build themselves. + +### Instance binding rules (revised 2026-06-12: url required) + +- `url` is **required** — every OCG tool binds the instance it talks to. + There is no server-side default instance (`agentspan.ocg.url`/`api-key` + were removed by decision the same day: server-wide instance config is + exactly the single-instance coupling this design exists to kill). +- `credential` names an entry in the credential store; the server resolves + it at execution time and sends `Authorization: Bearer `. The + secret never appears in Python code, serialized configs, or workflow + definitions — identical to the `http_tool` `${NAME}` model, and bounded + the same way (a caller can only name credentials their org has declared). + Omitting it means an unauthenticated instance. + +### Out of scope + +- TypeScript SDK parity — follow-up, same wire format. Until then, the + prompt/schemas live only in the Python SDK; non-SDK clients (REST, UI) + can inline the canonical agent JSON published in the docs. +- Dynamic per-*request* instance selection (one compiled tool, URL chosen at + call time). Instances are fixed at agent-definition time; per-request + routing is expressed as multiple tools (see router example above). + +--- + +## 2. Wire format and server flow + +`ocg_agent()` output is wrapped by the existing `agent_tool()`, so the parent +tool serializes with a full inline `agentConfig` — the standard path, nothing +OCG-specific. Each OCG tool inside it serializes as: + +```json +{ + "name": "ocg_query", + "description": "Query the Open Context Graph ...", + "inputSchema": {"type": "object", "properties": {"query": {...}, ...}, "required": ["query"]}, + "toolType": "ocg_query", + "config": { + "url": "https://us.ocg.example.com", + "credential": "OCG_US_KEY" + } +} +``` + +(`config` omitted entirely for default-instance tools. The serializer also +appends `config.credentials = ["OCG_US_KEY"]` — the existing wire key the +server reads to bound credential resolution for the execution token.) + +Server flow: + +- `registerAgentToolWorkflows()` compiles and registers the retriever child + workflow at start, exactly as for any inline agent tool + (`AgentService.java:1119-1163`). +- `ToolCompiler` already maps the seven `ocg_*` tool types to `OCG_*` task + types (`ToolCompiler.java:136-142`) and already includes `OCG_TOOL_TYPES` + in `serverSideTypes`. **New:** it gains an `ocgConfig` map (the `httpConfig` + pattern at `ToolCompiler.java:342-343`) carrying each OCG tool's + `url`/`credential` through to the task input, with the same credential- + placeholder escaping applied to HTTP/MCP headers. +- The `OCG_*` operations resolve their target per call: + 1. `url` from task config if present, else `OcgProperties.url`, else the + task FAILs with a clear message (§3 validation should have caught this + at start time — the runtime check is the backstop). + 2. Auth: `credential` from task config (resolved via the credential store) + if present, else `OcgProperties.apiKey`, else no auth header. +- Projection and response capping (`responseCapChars`) are unchanged and + shared across all instances. + +### Fail-fast validation at agent start + +In `AgentService.start()` / `startStreaming()`: if any tool (recursively, +including inline agent-tool children) has a type in `OCG_TOOL_TYPES` and +no `config.url` bound, reject the start request: +`"OCG tool 'ocg_query' has no OCG instance bound: set url= on +ocg_agent()/ocg_tools() in the SDK."` + +--- + +## 3. Server changes + +### Delete + +| Item | Notes | +|---|---| +| `compiler/AutoExposedToolsMerger.java` + tests | Auto-expose is gone entirely — no flag, no shim | +| `registry/RegisteredAgent.java` (incl. `ExposeAsTool`), `registry/RegisteredAgentRegistrar.java` + tests | OCG was the registry's only consumer; no boot-time workflow registration remains | +| `ocg/OcgRegisteredAgent.java` | | +| `ocg/OcgAgentFactory.java` | Prompt + schemas move to the SDK (§1); delete after the SDK copy lands, in the same PR, so there is never zero or two sources of truth | +| Merger/registrar call sites and Spring wiring in the compile path | | +| `agentspan.ocg.model` property + its fail-fast boot check | Model is now a required SDK parameter | + +### Modify + +| Item | Change | +|---|---| +| `ocg/operation/*` / `OcgRequestTask` | Read `url`/`credential` from task config with fallback to `OcgProperties` (§2); credential resolution via the existing store machinery | +| `OcgRegisteredTaskDefs` + `OcgRequestTaskConfig` | Registration no longer conditional on `OCG_URL` (there may be no global URL). Gate on `agentspan.ocg.enabled` (default `true`); `url`/`api-key` become the optional *default instance* | +| `AgentService` | Add the §2 start-time validation | +| `OcgProperties` | Drop `model`; document `url`/`apiKey` as default-instance only | + +### Keep unchanged + +- The seven `OCG_*` system task implementations' projection/capping logic. +- `ToolCompiler.TYPE_MAP` `ocg_*` entries — now the cross-repo wire contract + with the SDK. The seven tool-type strings get a Javadoc note that the SDK + depends on them; renames are breaking. + +### Behavior changes (intentional, breaking) + +1. Deployments relying on auto-expose lose the silent `ocg_agent` tool; + agents add `agent_tool(ocg_agent(model=..., ...))` (one import + one line). +2. The `_ocg_agent` workflow is no longer registered; anything referencing it + by name breaks. +3. `OCG_MODEL` is removed and ignored. + +No deprecation shims. Release notes + `docs/ocg-agent-flow.md` rewrite cover +migration. + +--- + +## 4. Pre-flight, revisited + +The original plan's server-side pre-flight was never implemented and stays +that way: "retrieve before the main agent acts" is now expressible in user +space — a sequential pipeline whose first stage is an `ocg_agent()`, or main- +agent instructions to call the retriever first. No server hook. + +--- + +## 5. Orkes-conductor integration + +Context: AgentSpan is being embedded into orkes-conductor as a direct +dependency (single app). [orkes-conductor PR #3673](https://github.com/orkes-io/orkes-conductor/pull/3673) +contains the current integration shim. + +### Tool resolution in `OrkesLLM` + +`OrkesLLM.getToolSpecs()` resolves tools **by name against the Orkes +integration store** — the natural contract for Orkes-native tools, which +*are* integrations/services/task-defs. AgentSpan-compiled tools are +**self-describing** (name, description, `inputSchema` inline in the LLM task +input) and aren't integrations, so the lookup dropped them; `ocg_agent` was +the first casualty, but any SDK-declared `agent_tool`/`http`/`mcp` tool hits +the same wall. + +Options considered: + +| # | Option | Assessment | +|---|---|---| +| 1 | **Schema-presence pass-through** (shipped in PR #3673): `inputSchema != null` ⇒ return the spec as-is, skip integration resolution | Correct for every tool this design produces — both `agent_tool(ocg_agent(...))` and raw `ocg_*` tools ship inline schemas. Weakness: schema presence is a heuristic; if Orkes-native tools ever populate `inputSchema`, resolution is silently skipped, and name collisions with integrations resolve silently in favor of the inline spec | +| 2 | **Explicit marker on `ToolSpec`** (`selfDescribing`), set by AgentSpan's `ToolCompiler.compileToolSpecs()`, branched on in `OrkesLLM` | **Selected.** The principled contract — intent declared, not inferred. Detailed design below | +| 3 | Register AgentSpan agents as Orkes integrations so name-lookup succeeds | **Rejected.** Abuses integration semantics, org-scoping is unanswerable, doesn't generalize to user-declared tools, and resolution would rewrite the compiled spec | +| 4 | AgentSpan ships its own LLM task type bypassing `OrkesLLM` | **Rejected.** Forks the LLM path inside the product we're embedding into; loses Orkes provider/integration management | + +**Decision:** adopt Option 2, with the marker transported in +`ToolSpec.configParams` (an existing `Map` field) rather +than a new model field — a first-class field would require a conductor-oss +release + dependency bump, which we choose not to wait for (decision +2026-06-12). Option 1's heuristic is then replaced outright — no +transition fallback is kept. Verified live: the top-level +`selfDescribing` key is stripped at `ToolSpec` deserialization, while +`configParams.selfDescribing` arrives intact in the bound LLM task input. +The top-level key is still emitted for a possible future first-class +field; nothing depends on it. Implementation detail + paste-ready orkes +changes: [2026-06-12-toolspec-selfdescribing-plan.md](2026-06-12-toolspec-selfdescribing-plan.md). + +### Option 2 in detail — `selfDescribing` on `ToolSpec` + +**The contract.** `selfDescribing = true` means: *this spec is complete as +delivered — name, description, and `inputSchema` are authoritative; consumers +must hand it to the LLM as-is and must not resolve, enrich, or replace it by +name against integrations, services, or task definitions.* It deliberately +says nothing about provenance ("compiled by AgentSpan") — any future producer +of complete inline specs (e.g. UI-authored tools) may set it, and consumers +other than `OrkesLLM` get the same instruction. Post-call routing of the +LLM's tool-call output stays where it already is: the workflow's tool router +(SWITCH), which never depended on integration resolution. + +**Three repos are touched** (the spec travels: AgentSpan compiles tool-spec +maps into the `LLM_CHAT_COMPLETE` task input → Conductor persists them → +`OrkesLLM` deserializes them into `ToolSpec` objects): + +1. **conductor-oss/conductor** — owns the model, `ai` module + (`ai/src/main/java/org/conductoross/conductor/ai/models/ToolSpec.java`, + a Lombok `@Data` POJO: `name`, `type`, `configParams`, + `integrationNames`, `description`, `inputSchema`, `outputSchema`). + Add one field: + + ```java + /** + * When true, this spec is complete as delivered: pass it to the LLM + * as-is. Consumers must not resolve, enrich, or replace it by name + * against integrations, services, or task definitions. Set by + * producers that compile full inline tool specs (e.g. AgentSpan). + */ + private boolean selfDescribing; + ``` + + `boolean` (not `Boolean`): absent on the wire ⇒ `false` ⇒ existing + name-resolution behavior. Lombok generates `isSelfDescribing()`. No + custom Jackson annotations needed — the field round-trips through the + task-input map like every other field, and LLM providers ignore it when + building their native tool definitions (they read name / description / + inputSchema only). + +2. **agentspan** — the producer. `ToolCompiler.compileToolSpecs()` + (`ToolCompiler.java:157`) stamps every compiled spec: + + ```java + spec.put("selfDescribing", true); + ``` + + Unconditional — every AgentSpan tool spec is self-describing by + construction (the compiler always emits complete name + description + + `inputSchema`, for all tool types: `agent_tool`, `http`, `mcp`, `ocg_*`, + worker, …). No per-type logic. + +3. **orkes-conductor** — the consumer. `OrkesLLM.getToolSpecs()` replaces + the PR #3673 heuristic outright: + + ```java + if (toolSpec.isSelfDescribing()) { + // Self-describing spec: complete as delivered — hand it to the + // LLM as-is, never resolve by name. + return List.of(toolSpec); + } + // existing name-based integration resolution below, unchanged + ``` + +**Name-collision semantics become defined behavior:** a self-describing tool +whose name matches a registered integration is passed through — the inline +spec wins, by declared intent rather than by accident of schema presence. +Orkes-native tools without the marker resolve exactly as today, even if they +someday carry schemas — schema presence means nothing to `OrkesLLM` once +the marker branch lands. + +**Rollout and skew.** In the single app, producer (AgentSpan dependency) and +consumer (`OrkesLLM`) upgrade atomically in one deploy, so the only skew is +*data* skew: `LLM_CHAT_COMPLETE` task inputs compiled before the upgrade but +executed after. Two facts bound it: + +- Agent workflow definitions are re-registered on every agent start + (`AgentService.start()` → `updateWorkflowDef`), so every start after the + upgrade compiles fresh specs carrying the marker. Only executions already + mid-flight at deploy time carry markerless specs — those fall through to + name resolution and are dropped, an accepted, hours-bounded window + (no fallback is kept; decision 2026-06-12). +- Conductor's `ObjectMapperProvider` ignores unknown JSON properties, so a + marker-stamped spec deserializes cleanly even against an old `ToolSpec` + (the marker is simply dropped). The required landing order: conductor-oss + field → orkes-conductor dependency bump + consumer branch; until both + land, the PR #3673 heuristic stays in place as the working path. + +**Tests** (fail-first per root `CLAUDE.md`): + +- agentspan unit: every spec returned by `compileToolSpecs()` — across all + tool types — carries `selfDescribing: true` (make it fail first by + asserting a wrong key, e.g. `self_describing`). +- orkes-conductor unit, `OrkesLLM.getToolSpecs()`: + marked spec → passed through, integration lookup **never invoked** + (verify on the mocked integration service); unmarked spec (with or + without schema) → existing resolution path; marked spec whose name + matches a registered integration → inline spec wins. + +### Multi-tenancy fit + +Per-tool instance binding is the only option that matches Orkes' tenancy +model: application properties are app-wide and operator-level, while Orkes +**credential stores are org-scoped**. With this design a tenant self-serves — +they store `OCG_US_KEY` in their org's credential store and reference it from +their agent code; no operator config change, no restart, and credential +resolution is bounded to names their org declared. A named-instances-in- +server-config alternative (`agentspan.ocg.instances.us.url=...`) was +considered and rejected: every tenant onboarding would be an operator-level +config change to an app-wide namespace. + +### Housekeeping in orkes-conductor (follow-up PR) + +- Replace the `application.properties` OCG block from PR #3673: + + ```properties + # ============================================================================= + # OCG (Open Context Graph) Configuration + # ============================================================================= + # OCG agents and tools are declared in user code via the AgentSpan SDK + # (ocg_agent() / ocg_tools()), each binding its own OCG instance URL and + # credential-store reference. The properties below only configure the + # OPTIONAL server-wide default instance, used by tools that don't set url=. + agentspan.ocg.enabled=${OCG_ENABLED:false} + ``` + +- Remove `agentspan.ocg.model=${OCG_MODEL:}` and its fail-fast. +- Replace the PR #3673 `OrkesLLM` heuristic with the `selfDescribing` branch + (no fallback — Option 2 detail above), bumping the conductor-oss + dependency to the version carrying the `ToolSpec` field. + +--- + +## 6. Implementation stages + +Per SDK plan conventions: validation is a separate stage before documentation. + +**Stage 1 — Server: per-instance execution + removals** +1. `OcgRequestTask`/operations: per-call `url`/`credential` resolution with + `OcgProperties` fallback; credential resolved via the existing store path. +2. `ToolCompiler`: `ocgConfig` plumbing with credential-placeholder escaping; + stamp `selfDescribing: true` on every spec in `compileToolSpecs()` (§5). +3. Task-def registration gated on `agentspan.ocg.enabled`, not `url`. +4. Delete auto-expose + registry + `OcgRegisteredAgent`/`OcgAgentFactory`, + strip wiring, drop `agentspan.ocg.model`. +5. Add the start-time "no instance" validation. + +**Stage 2 — SDK: `agentspan/agents/ocg.py`** +1. `OCG_SYSTEM_PROMPT` + seven tool schemas moved verbatim from + `OcgAgentFactory`. +2. `ocg_tools()` (instance binding, subset switches, `credential`-without- + `url` rejection), `ocg_agent()`; export from `agentspan.agents`. + +**Stage 3 — Validation** (before any docs) +- Per root `CLAUDE.md`: every test is written first, made to fail (wrong wire + key, wrong tool-type string, swapped instance URLs), the failure asserted, + then fixed. No LLM-judged validation in e2e — assert on workflow/tool + structure and recorded HTTP traffic, not model output quality. +- Server unit: OCG operation hits per-tool URL when configured, falls back to + `OcgProperties.url`, FAILs cleanly with neither; credential from config + resolves via the store, falls back to `apiKey`. +- Server unit: compile with OCG enabled → **no** OCG tool appears unless + declared (inverse of the old merger test); start-time validation rejects + instanceless OCG tools with the documented message. +- SDK unit: `ocg_agent()` returns an Agent whose tools carry the expected + `tool_type`s and `config`; `ocg_tools(memory=False)` returns exactly 4 + defs; `credential` without `url` raises; `url=None` emits no `config`. +- Server unit: every spec from `compileToolSpecs()` carries + `selfDescribing: true` (§5 test list; the orkes-conductor consumer tests + live in that repo's PR). +- e2e (`sdk/python/e2e/`): **two** stub OCG instances (WireMock or the + `e2e/ocg` harness); a US agent and a Canada agent each with their own + `ocg_agent(...)` — assert each retriever's `OCG_QUERY` traffic lands on its + own stub and only that stub (the multi-tenancy guarantee). Negative e2e: + agent without OCG tools → no OCG dispatch. + +**Stage 4 — Documentation** +- Rewrite `docs/ocg-agent-flow.md` (currently documents auto-expose and the + boot-registered `_ocg_agent`). +- Python SDK API reference for `ocg_agent()` / `ocg_tools()`, including the + multi-instance naming requirement and a canonical agent-JSON snippet for + non-SDK clients. +- Release note for the breaking changes (§3). +- Cross-repo PRs, in strict landing order (§5): conductor-oss + `ToolSpec.selfDescribing` field → orkes-conductor dependency bump + + `OrkesLLM` branch + properties housekeeping. The consumer branch cannot + ship before the field exists (no fallback is kept), and the PR #3673 + heuristic stays in place until it does. diff --git a/docs/design/2026-06-12-ocg-sdk-subagent-status.md b/docs/design/2026-06-12-ocg-sdk-subagent-status.md new file mode 100644 index 000000000..1f75ccf20 --- /dev/null +++ b/docs/design/2026-06-12-ocg-sdk-subagent-status.md @@ -0,0 +1,181 @@ +# OCG SDK Sub-Agent — Implementation Status + +**Date:** 2026-06-12 +**Design:** [2026-06-12-ocg-sdk-subagent-design.md](2026-06-12-ocg-sdk-subagent-design.md) + +--- + +## Accomplished + +### Server — Stage 1 (complete) + +**Per-instance execution:** +- `OcgTarget` record (`ocg/operation/OcgTarget.java`) — the resolved + instance (base URL + Authorization header) an operation runs against. + All seven operations, `OcgRequest`, and `OcgUri` now take `OcgTarget` + instead of `OcgProperties`; operations can no longer reach the default + config. +- `OcgRequestTask` resolves the target per call: reserved task inputs + `__ocg_url` / `__ocg_auth` (compiled from the SDK's `url=` / + `credential=`) win over the `OcgProperties` default; with neither, the + task fails with the documented "no OCG instance" message. Reserved keys + are stripped before operations see the input (so `ocg_memory_set` can't + leak them into request bodies). +- Credential resolution: `OcgCredentialResolver` + + `PlaceholderCredentialResolver` resolve `#{NAME}` placeholders through + the credential store scoped by the execution token (same contract as + `CredentialAwareHttpTask`); resolved values exist only in memory. In + embedded mode the host resolves `${workflow.secrets.NAME}` before the + task starts. An unresolvable placeholder fails the task — it is never + sent as a bearer token. +- `ToolCompiler.buildEnrichTask` bakes `url` + escaped auth placeholder + into the `ocgConfig` entry; the enrich script (both copies in + `JavaScriptBuilder`) merges them into dispatched task inputs. + +**`selfDescribing` marker (agentspan's share of OrkesLLM Option 2):** +- `ToolCompiler.compileToolSpecs()` stamps every compiled spec twice: + top-level `selfDescribing: true` (future first-class field) AND + `configParams.selfDescribing: true` — the copy that survives `ToolSpec` + deserialization today (verified live). Merged into existing MCP/API + `configParams`, never clobbering them. + +**Auto-expose + registered-agent machinery deleted:** +- Deleted: `AutoExposedToolsMerger`, `RegisteredAgent` (+ `ExposeAsTool`), + `RegisteredAgentRegistrar`, `OcgRegisteredAgent`, `OcgAgentFactory` + (prompt/schemas moved verbatim to the SDK), and their tests + (`AutoExposedToolsMergeTest`, `RegisteredAgentBootstrapTest`). +- `AgentCompiler.compile()` is the single entry point; + `compileWithoutAutoExpose` removed (callers updated, incl. + `MultiAgentCompiler`). `RegisteredTaskDefs` / `RegisteredTaskDefsRegistrar` + remain (the task-def half of the registry is still used). + +**Gating + config:** +- `OcgRequestTaskConfig` and `OcgRegisteredTaskDefs` now gate on + `agentspan.ocg.enabled` (default `true`), not on `OCG_URL` — tasks must + exist even with no default instance. +- `OcgProperties`: `model` removed; `enabled` added; `isEnabled()` renamed + `hasDefaultUrl()` (the Lombok-generated `isEnabled()` now reflects the + `enabled` flag). `url`/`api-key` documented as the optional default + instance. +- `application.properties`: OCG block rewritten (adds `OCG_ENABLED`, + drops `OCG_MODEL`, documents SDK-side declaration). + +**Start-time fail-fast:** +- `OcgToolValidator` — rejects agent starts whose OCG tools have no bound + `url` (there is no server-side default instance), and any OCG tool when + `agentspan.ocg.enabled=false`. Walks sub-agents and inline `agent_tool` + children (both typed `AgentConfig` and raw SDK-serialized maps). Wired + into `AgentService.start()`. + +### SDK — Stage 2 (complete) + +- New module `sdk/python/src/agentspan/agents/ocg.py`: + - `OCG_SYSTEM_PROMPT` — moved verbatim from `OcgAgentFactory`, including + the execution-time `${workflow.input.__today__}` date anchor and the + open-range `end_time` guidance. The SDK is now the only home of the + prompt and schemas. + - `ocg_tools(url=, credential=, query=, entities=, code_history=, memory=)` + — the seven raw `ToolDef`s with the canonical schemas; instance binding + lands in each tool's `config` and declares the credential name for + execution-token bounding. `credential` without `url` raises + `ValueError`. + - `ocg_agent(model=, name=, url=, credential=, instructions=, max_turns=, …)` + — prebuilt retrieval `Agent`; `model` is keyword-required (no silent + default). Docstrings carry the multi-instance distinct-name warning. + - Exported from `agentspan.agents`. + +### Validation — Stage 3 (complete) + +Per root `CLAUDE.md`, every new behavior test was written first, run, and +its failure observed before the implementation landed (e.g. the +ToolCompiler instance-config and selfDescribing tests failed 2/16; the +`OcgRequestTask` per-instance tests failed 6/8; the validator tests failed +4/7 against a stub) — then turned green. + +- Server (`conductor-agentspan-server` suite — **all green**, incl. the + full pre-existing suite): + - `OcgRequestTaskTest` (8) — URL override / fallback / no-instance + failure message, pre-resolved + placeholder + unresolvable auth, + default api-key, reserved-input stripping from request bodies. + - `OcgToolValidatorTest` (7) — per-tool url, default fallback, rejection + messages, feature-disabled, inline-child map walk, sub-agent walk. + - `ToolCompilerTest` (+3) — every spec `selfDescribing`, ocgConfig + carries url + `Bearer #{NAME}` (standalone escaping), default-instance + entry stays minimal. + - `AgentCompilerTest` (+1) — inverse of the deleted merger tests: + compile never injects undeclared tools. +- SDK: `tests/unit/test_ocg.py` (16, **all green**) — subset switches, + instance binding in config + declared credentials, `credential`-without- + `url` rejection, schema required-fields, prompt anchor survival, exports, + and an end-to-end serialization test proving the wire shape + (`toolType: ocg_query`, `config.url/credential/credentials`) that the + server-side `ToolCompiler` consumes. +- Note: the full SDK unit suite has ~138 pre-existing failures under the + system Python — identical count on a clean tree; unrelated to this + change. Under the `uv` env, adjacent suites pass. `ruff format` + + `ruff check` clean on all new SDK files. +- **e2e (`sdk/python/e2e/test_suite22_ocg.py`) — 3/3 green** against a + live server running this build: two stub OCG instances; the US-bound + retriever's `OCG_QUERY` traffic landed only on the US stub, the + Canada-bound only on the Canada stub, and the no-OCG agent produced zero + OCG traffic. Validation is recorded HTTP traffic, never LLM-judged. +- The e2e caught and fixed a real pre-existing bug: `OcgRegisteredTaskDefs` + registered TaskDefs under the operation labels (`query`, `memory_set`, …) + while dispatch schedules tasks named `taskType.toLowerCase()` + (`ocg_query`, …) — every OCG dispatch failed with "Cannot find task by + name ocg_query". Fixed (names now derive from `TASK_TYPE`), covered by + `OcgRegisteredTaskDefsTest` (fail-first), verified live. + +### Documentation — Stage 4 (complete) + +- `docs/ocg-agent-flow.md` rewritten for the SDK-declared design: usage, + multi-instance binding, custom retrieval agents, server setup + (`enabled`/default instance, no `OCG_MODEL`), execution flow incl. + credential handling, the operations table, and a migration-from- + auto-expose section. +- `docs/python-sdk/api-reference.md` — new "ocg_agent() / ocg_tools()" + section under Tools (parameters table, multi-instance example, custom + agent example, non-SDK JSON note). +- `docs/release-notes/2026-06-12-ocg-sdk-subagent.md` — breaking changes, + new capabilities, the TaskDef-naming fix, and a migration table. + +### Empirical finding worth knowing (cross-repo) + +On the live server, the *top-level* `selfDescribing` key is dropped when +the spec deserializes through `org.conductoross.conductor.ai.models.ToolSpec` +(no such field; unknown keys ignored) — but `configParams.selfDescribing` +survives intact in the bound LLM task input (verified on a live +execution). That made `configParams` the chosen transport: **no +conductor-oss change or release is needed**; `OrkesLLM` reads the marker +from `getConfigParams()`. + +--- + +## Pending + +1. **orkes-conductor `selfDescribing` consumer — planned, unblocked.** + No conductor-oss work required (the marker rides `configParams`; see + the empirical finding above). Paste-ready plan at + [`2026-06-12-toolspec-selfdescribing-plan.md`](2026-06-12-toolspec-selfdescribing-plan.md): + `OrkesLLM` reads `configParams.selfDescribing` and the heuristic is + removed outright (no fallback, per 2026-06-12 decision), plus the + `application.properties` OCG block update. Lands on PR #3673's branch + whenever ready. A first-class `ToolSpec` field remains optional future + cleanup, nothing depends on it. +2. **TypeScript SDK parity** — explicitly out of scope in the design; + same wire format when picked up. +3. **Commit the work** — everything above is uncommitted on + `feature/OCG_System_Task`. The deployed local server at + `~/.agentspan/server/agentspan-runtime.jar` runs this build (previous + jar preserved as `agentspan-runtime.jar.bak`). + +--- + +## Addendum (2026-06-12, later): server-side default instance removed + +Per user decision, the optional server-wide default OCG instance +(`agentspan.ocg.url` / `api-key`) is gone entirely. `url=` is now required +on `ocg_agent()`/`ocg_tools()`; `OcgProperties` is just +`enabled` + `responseCapChars`; `OcgRequestTask`/`OcgToolValidator` have no +fallback. SDK, examples, smoke, and docs updated; full server suite + SDK +unit tests green; republished to mavenLocal. diff --git a/docs/design/2026-06-12-toolspec-selfdescribing-plan.md b/docs/design/2026-06-12-toolspec-selfdescribing-plan.md new file mode 100644 index 000000000..a3545d1ad --- /dev/null +++ b/docs/design/2026-06-12-toolspec-selfdescribing-plan.md @@ -0,0 +1,143 @@ +# Plan: `selfDescribing` Tool-Spec Marker — orkes-conductor only + +**Date:** 2026-06-12 (revised same day: no conductor-oss dependency) +**Status:** Ready — agentspan's producer side is merged and verified live +**Parent design:** [2026-06-12-ocg-sdk-subagent-design.md](2026-06-12-ocg-sdk-subagent-design.md) §5 + +## Context + +AgentSpan compiles **self-describing** tool specs — name, description, and +`inputSchema` ship inline in the `LLM_CHAT_COMPLETE` task input. Orkes' +`OrkesLLM` worker resolves tools **by name against the integration store**, +so AgentSpan specs were dropped until +[orkes-conductor PR #3673](https://github.com/orkes-io/orkes-conductor/pull/3673) +added a heuristic: `inputSchema != null` ⇒ pass the spec through. + +The durable contract is an explicit marker meaning: *this spec is complete +as delivered — hand it to the LLM as-is; do not resolve, enrich, or replace +it by name against integrations, services, or task definitions.* + +## Transport: `configParams`, not a new field + +A first-class `ToolSpec.selfDescribing` field would require a conductor-oss +change + release + `revConductor` bump (conductor-ai is a Maven dependency +of orkes-conductor, not vendored). **Decision (2026-06-12): don't wait for +that.** `ToolSpec` already has `configParams: Map` — a +generic map that survives deserialization — so the marker rides there. + +AgentSpan (already merged + verified) stamps every compiled spec with both: + +```json +{ + "name": "ocg_query", + "type": "OCG_QUERY", + "selfDescribing": true, // future-proofing only — dropped today + "configParams": {"selfDescribing": true}, // the copy that survives + ... +} +``` + +**Verified live:** in a bound `LLM_CHAT_COMPLETE` task input on a running +server, the top-level key is stripped at `ToolSpec` deserialization +(`ObjectMapperProvider` drops unknown properties) while +`configParams.selfDescribing == true` arrives intact. For MCP/API tools the +marker is merged into their existing `configParams` (mcpServer/baseUrl +entries untouched). + +The top-level key stays in the compiled spec so that if a first-class field +ever lands upstream, consumers can switch to `isSelfDescribing()` with no +producer change — but nothing depends on it. + +--- + +## The orkes-conductor change (unblocked, single repo) + +Branch: the agentspan-embed work already lives on +`feature/agentspan-embed-spike-nich-changes` (PR #3673) — push there, or +stack a follow-up PR if #3673 merges first. + +### Change 1 — `workers/src/main/java/io/orkes/conductor/enterprise/workers/integrations/OrkesLLM.java` + +Replace the PR #3673 heuristic at the top of `getToolSpecs(...)` — the +`inputSchema != null` check is removed entirely (no fallback kept): + +```java + private List getToolSpecs(String orgId, ToolSpec toolSpec) { + if (isSelfDescribing(toolSpec)) { + // Self-describing spec (e.g. compiled by AgentSpan): complete + // as delivered — hand it to the LLM as-is, never resolve by + // name against integrations/services/taskdefs. + return List.of(toolSpec); + } + // existing name-based integration resolution below, unchanged + ... + } + + /** + * The marker rides configParams because ToolSpec has no dedicated + * field; a generic map entry survives deserialization where an + * unknown top-level key would be dropped. + */ + private static boolean isSelfDescribing(ToolSpec toolSpec) { + Map cp = toolSpec.getConfigParams(); + return cp != null && Boolean.TRUE.equals(cp.get("selfDescribing")); + } +``` + +Defined name-collision semantics: a self-describing spec whose name matches +a registered integration passes through (inline wins, by declared intent). + +Note: markerless specs fall through to name resolution and are dropped — +that only affects executions already mid-flight at upgrade time, since +agent workflows re-register on every start and any new start carries the +marker. + +### Change 2 — `server/src/main/resources/application.properties` + +Replace the OCG block added in PR #3673 (it documents auto-expose and +requires `OCG_MODEL`, both removed from agentspan). DONE in the local +checkout — the block is now just the enable switch (every OCG tool binds +its own instance from the SDK; there is no server-side instance config): + +```properties +# ============================================================================= +# OCG (Open Context Graph) Configuration +# ============================================================================= +# OCG agents and tools are declared in user code via the AgentSpan SDK +# (ocg_agent() / ocg_tools()), each binding its own OCG instance URL and +# credential-store reference. There is no server-side OCG configuration +# beyond this switch, which registers the OCG_* system tasks. +agentspan.ocg.enabled=${OCG_ENABLED:false} +``` + +(`agentspan.ocg.model`/`url`/`api-key` are removed and ignored by +agentspan. The host defaults the switch to false — set `OCG_ENABLED=true` +in the runtime environment to turn the execution layer on.) + +### Tests (fail-first) + +`OrkesLLM.getToolSpecs()` unit tests: +- spec with `configParams.selfDescribing == true` → passed through; + integration lookup **never invoked** (verify on the mocked integration + service); +- spec without the marker (with or without schema, with or without other + configParams entries) → existing name-resolution path; +- marked spec whose name matches a registered integration → inline wins; +- MCP-shaped spec (`configParams` carrying `mcpServer` **and** the marker) + → passed through with configParams intact. + +After this lands, schema presence means nothing to `OrkesLLM`; the +configParams marker is the sole contract. + +--- + +## Optional future cleanup (conductor-oss, no urgency) + +If/when convenient, add `private boolean selfDescribing` to +`ai/src/main/java/org/conductoross/conductor/ai/models/ToolSpec.java` +(Javadoc: complete-as-delivered semantics, primitive boolean for +backward-compatible absence). AgentSpan already emits the top-level key, so +on a dependency bump `OrkesLLM` can simplify to +`toolSpec.isSelfDescribing() || configParamsMarker(toolSpec)` and the +configParams transport can be retired one release later. None of the above +waits on this. diff --git a/docs/index.md b/docs/index.md index 5f75ec8af..69a5b4cad 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,7 +29,7 @@ Agentspan is a durable runtime for AI agents. Execution state lives server-side, - [Deployment overview](deployment.md) - Local development, Docker, Helm, and Orkes Cloud. - [Self-hosting](self-hosting.md) - Run Agentspan in your own environment. -- [OCG Sub-Agent integration](ocg-agent-flow.md) - Set `OCG_URL` + `OCG_API_KEY` to give every user agent a built-in retrieval tool over the Open Context Graph. +- [OCG Sub-Agent integration](ocg-agent-flow.md) - Declare a retrieval sub-agent over the Open Context Graph from the SDK (`ocg_agent(url=..., credential=...)`). ## Examples diff --git a/docs/ocg-agent-flow.md b/docs/ocg-agent-flow.md index da1be1a38..19af74916 100644 --- a/docs/ocg-agent-flow.md +++ b/docs/ocg-agent-flow.md @@ -1,276 +1,168 @@ # OCG Sub-Agent -A built-in retrieval sub-agent that any user's LLM can delegate to mid-loop -when it needs context from the Open Context Graph (OCG) — Slack messages, -Jira tickets, code history, stored memories. Enabled by setting -`OCG_URL`. Disabled by leaving it unset. +A retrieval sub-agent over the Open Context Graph (OCG) — message search, +entity lookup, code history, stored memories — that any agent **opts into +from the SDK**. The SDK is the canonical home of the OCG agent (system +prompt, tool schemas, instance binding); the server provides only the +execution layer: seven `OCG_*` system tasks that make the authenticated +HTTP calls, resolve credentials, project response fields, and cap response +sizes. -The feature is also the **first consumer of a generic -`RegisteredAgent` registry pattern**: any future server-side sub-agent -plugs in as one `@Component` without touching `AgentCompiler`, -`AgentService`, or any per-feature `@PostConstruct` boilerplate. +Nothing is auto-injected: an agent that doesn't declare OCG tools never +makes an OCG call. (The previous design — a server-registered `_ocg_agent` +silently appended to every agent when `OCG_URL` was set — is gone; see +[Migration](#migration-from-auto-expose).) ---- - -## Setup — integrating OCG with AgentSpan - -OCG is fully opt-in. The integration is **three environment variables** the -AgentSpan server reads at startup: - -| Env var | Required? | What it does | -| ------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- | -| `OCG_URL` | **Yes** (to enable) | Base URL of your OCG instance, e.g. `https://dev.orkescontextgraph.io`. If unset or empty, every OCG bean stays out of the Spring context, no `_ocg_agent` workflow is registered, and no user agent gets the auto-injected `ocg_agent` tool. The feature is completely dormant. | -| `OCG_MODEL` | **Yes** (when OCG is enabled) | LLM the OCG sub-agent uses for its own turns, e.g. `openai/gpt-4o-mini`, `anthropic/claude-haiku-4-5`. No silent default — boot fails fast with a clear error if `OCG_URL` is set but `OCG_MODEL` is blank. The right model depends on cost/latency targets and the OCG corpus you're querying, so it has to be an explicit operator decision. | -| `OCG_API_KEY` | Yes (if OCG requires auth) | Bearer token sent as `Authorization: Bearer ` on every OCG HTTP request. Empty means no auth header — fine for unauthenticated local OCG instances; required for the hosted dev / prod instances. | - -### Local dev +Design history: [`design/2026-06-12-ocg-sdk-subagent-design.md`](design/2026-06-12-ocg-sdk-subagent-design.md). -When starting the server (via `./gradlew bootRun`, IntelliJ run config, -or `java -jar`): - -```bash -export OCG_URL=https://dev.orkescontextgraph.io -export OCG_API_KEY= -export OCG_MODEL=openai/gpt-4o-mini # required when OCG is enabled -export OPENAI_API_KEY=sk-... # provider key for whatever OCG_MODEL points at -./gradlew bootRun -``` +--- -In IntelliJ, add the same four to your Spring Boot run configuration's -**Environment variables** field. +## Using OCG from the SDK -### Docker / production +Delegate retrieval from a main agent: -Pass them through whatever your deployment system uses — docker `-e`, -Kubernetes `env`, Helm values, systemd `EnvironmentFile`, etc. They map -to Spring properties via `application.properties`: +```python +from agentspan.agents import Agent, agent_tool +from agentspan.agents.ocg import ocg_agent -``` -agentspan.ocg.url=${OCG_URL:} -agentspan.ocg.api-key=${OCG_API_KEY:} -agentspan.ocg.model=${OCG_MODEL:} +retriever = ocg_agent(model="openai/gpt-4o-mini", + url="https://ocg.example.com", credential="OCG_KEY") +main = Agent( + name="support", + model="openai/gpt-4o", + tools=[agent_tool(retriever)], + instructions="...", +) ``` -So you can alternatively pass them as Spring properties on the JVM -command line (`-Dagentspan.ocg.url=…`) or via a `SPRING_APPLICATION_JSON` -blob if your platform prefers that. +The main agent's LLM sees one tool (named after the retriever); calling it +dispatches the retrieval agent as a SUB_WORKFLOW, which runs its own LLM +loop with the seven `ocg_*` tools and returns a synthesized, cited answer. -### Verifying it's enabled +### Multi-instance (data residency / multi-tenancy) -After the server starts with `OCG_URL` set you should see these three -lines in the log: +Each retriever binds its own OCG instance — different agents can target +different graphs: +```python +us = ocg_agent(name="ocg_us", model="openai/gpt-4o-mini", + url="https://us.ocg.example.com", credential="OCG_US_KEY") +ca = ocg_agent(name="ocg_canada", model="openai/gpt-4o-mini", + url="https://ca.ocg.example.com", credential="OCG_CA_KEY") ``` -INFO dev.agentspan.runtime.registry.RegisteredTaskDefsRegistrar — Registered 7 TaskDef(s) from 1 supplier(s) -INFO dev.agentspan.runtime.registry.RegisteredAgentRegistrar — Registered agent: workflow='_ocg_agent' autoExposeAs='ocg_agent' -INFO dev.agentspan.runtime.registry.RegisteredAgentRegistrar — Registered 1 server-side agent(s) -``` - -Two quick HTTP checks: - -```bash -# 1. The OCG sub-agent workflow is registered -curl -s http://localhost:6767/api/metadata/workflow/_ocg_agent | jq .name -# → "_ocg_agent" -# 2. The OCG primitive TaskDefs are registered -curl -s -o /dev/null -w "%{http_code}\n" http://localhost:6767/api/metadata/taskdefs/ocg_query -# → 200 +- `url` — the OCG instance this retriever (and only this retriever) talks + to. Required: every OCG tool binds its own instance; there is no + server-side default. +- `credential` — the **name** of a credential-store entry holding the OCG + bearer token. The server resolves it at execution time; the secret never + appears in Python code, serialized configs, or persisted workflow + definitions. Requires `url`. +- **Names must be distinct per instance.** Inline `agent_tool` child + workflows are registered by agent name — two differently-bound agents + sharing a name overwrite each other's workflow definition. + +### Custom retrieval agents + +`ocg_agent()` is just an `Agent` factory. For full control take the raw +tools and build your own: + +```python +from agentspan.agents.ocg import ocg_tools + +my_retriever = Agent( + name="retriever", + model="anthropic/claude-haiku-4-5", # your model choice + instructions="My custom retrieval prompt...", + tools=ocg_tools(url="https://us.ocg.example.com", + credential="OCG_US_KEY", + memory=False), # retrieval-only subset +) ``` -If `OCG_URL` is unset, both endpoints return 404 — that's the disabled -state. +Subset switches: `query`, `entities` (get_entity + neighborhood), +`code_history`, `memory` (set/reinforce/delete). -### Optional tuning knobs +### Pre-flight retrieval -| Property | Default | Effect | -| ---------------------------------- | ------- | ---------------------------------------------------------------------------- | -| `agentspan.ocg.response-cap-chars` | `8192` | Per-call response truncation budget. Raise if your model context allows; lower to save tokens. | +"Retrieve before the main agent acts" is expressed in user space — make the +retriever the first stage of a sequential pipeline, or instruct the main +agent to call its retrieval tool first. There is no server-side pre-flight +hook. --- -## 1. Architecture in one picture - -```mermaid -flowchart TB - subgraph L4["Layer 4 — Generic auto-expose (OCG-agnostic)"] - AC["AgentCompiler +
    AutoExposedToolsMerger
    (reads RegisteredAgent beans)"] - RAR["RegisteredAgentRegistrar
    (picks up RegisteredAgent beans)"] - RTR["RegisteredTaskDefsRegistrar
    (picks up RegisteredTaskDefs beans)"] - end - subgraph L3["Layer 3 — OCG sub-agent definition"] - ORA["OcgRegisteredAgent
    @Component"] - ORT["OcgRegisteredTaskDefs
    @Component"] - OAF["OcgAgentFactory
    (builds the AgentConfig)"] - end - subgraph L2["Layer 2 — OCG primitive system tasks"] - OcgReq["OcgRequestTask × 7
    (OCG_QUERY, OCG_GET_ENTITY, …)"] - Strats["Strategy classes:
    OcgQueryOperation,
    OcgGetEntityOperation, …"] - end - subgraph L1["Layer 1 — Configuration"] - Props["OcgProperties
    (url, apiKey, model, responseCapChars)"] - Cond["@ConditionalOnExpression
    ('${agentspan.ocg.url:}'.length() > 0)"] - end - - L1 --> L2 - L1 --> L3 - L3 --> L4 - L2 -.exposes task types.-> L4 -``` - -Reading bottom-up: each layer is independent of the ones above it. -**Removing OCG entirely means deleting layers 1-3; layer 4 stays generic -and useful for any other server-side sub-agent.** +## Server setup ---- - -## 2. Startup — the registry does the work - -```mermaid -sequenceDiagram - autonumber - participant SB as Spring Boot - participant OcgBeans as OcgRegisteredAgent +
    OcgRegisteredTaskDefs
    (@Component, gated by url) - participant TaskCfg as OcgRequestTaskConfig
    (gated by url) - participant TDR as RegisteredTaskDefsRegistrar
    (generic) - participant AR as RegisteredAgentRegistrar
    (generic) - participant AC as AgentCompiler - participant DAO as MetadataDAO - - SB->>+OcgBeans: instantiate (URL is set) - deactivate OcgBeans - SB->>+TaskCfg: instantiate — 7 OCG_* system task beans - deactivate TaskCfg - - Note over TDR,AR: @DependsOn ensures TaskDefs run first - - SB->>+TDR: @PostConstruct - TDR->>+OcgBeans: taskDefs() - OcgBeans-->>-TDR: 7 TaskDefs
    (ocg_query, ocg_get_entity, …) - TDR->>+DAO: updateTaskDef × 7 - DAO-->>-TDR: ok - deactivate TDR - - SB->>+AR: @PostConstruct - AR->>+OcgBeans: agentConfig() - OcgBeans-->>-AR: AgentConfig("_ocg_agent") - AR->>+AC: compileWithoutAutoExpose(AgentConfig) - AC-->>-AR: WorkflowDef "_ocg_agent" - AR->>+DAO: updateWorkflowDef - DAO-->>-AR: ok - deactivate AR - Note over DAO: _ocg_agent is now dispatchable
    by name at runtime -``` +The server side is the execution layer only. Configuration +(`application.properties` / env): -The registrars know nothing about OCG. They iterate `List` -and `List` provided by Spring, run each through a -fixed pipeline (compile + persist for agents; persist for TaskDefs), and -call it a day. OCG just happens to be the one feature providing those -beans today. +| Property | Env var | Default | What it does | +| --- | --- | --- | --- | +| `agentspan.ocg.enabled` | `OCG_ENABLED` | `true` | Registers the seven `OCG_*` system tasks and `ocg_*` TaskDefs. When `false`, agent starts that declare OCG tools are rejected with a clear error. | +| `agentspan.ocg.response-cap-chars` | — | 8192 | Post-projection truncation cap per response. | -LLM visibility is handled separately: `AutoExposedToolsMerger` reads -`autoExpose()` straight from the same `RegisteredAgent` bean list at -construction, so user compiles see `ocg_agent` regardless of when these -DAO writes happen — there is no startup-ordering dependency between -registration and visibility. +That's the whole server surface. There is no server-side OCG instance +configuration: no `OCG_URL`/`OCG_API_KEY` (every tool binds its own +instance from the SDK) and no `OCG_MODEL` (the retrieval agent's model is +a required SDK parameter, `ocg_agent(model=...)`). ---- +### Verifying it's enabled -## 3. Compile-time merge — how every user agent gets `ocg_agent` - -```mermaid -sequenceDiagram - autonumber - participant U as Client - participant AS as AgentService - participant AC as AgentCompiler - participant DAO as MetadataDAO - - U->>+AS: POST /api/agent/start {agentConfig, prompt} - AS->>AS: resolveConfig() — normalize framework - AS->>+AC: compile(config) - - Note over AC: AutoExposedToolsMerger.merge(config)
    entries fixed at construction from
    the RegisteredAgent bean list — no DAO read - loop for each auto-exposed RegisteredAgent - alt name != config.name, not duplicate - AC->>AC: append ToolConfig{
    name: expose.toolName
    toolType: "agent_tool"
    config.workflowName: agent workflow name
    } - end - end - - AC->>AC: strategy dispatch (compileSimple / compileWithTools / …)
    ocg_agent → SUB_WORKFLOW handler at runtime - AC-->>-AS: WorkflowDef - AS->>+DAO: updateWorkflowDef - DAO-->>-AS: ok - AS->>+DAO: startWorkflow - DAO-->>-AS: executionId - AS-->>-U: 200 {executionId} +```bash +# The seven OCG TaskDefs are registered (names match dispatch): +curl -s localhost:6767/api/metadata/taskdefs \ + | jq '[.[].name | select(startswith("ocg_"))]' +# → ["ocg_query", "ocg_get_entity", ..., "ocg_memory_delete"] ``` -Guards inside the merger: - -| Guard | Why | -| -------------------------- | ------------------------------------------------------------------ | -| No `RegisteredAgent` beans | Unit tests using `new AgentCompiler()` should still work | -| Self-recursion | Re-compiling `_ocg_agent` itself won't inject itself as a tool | -| Duplicate name | A caller's explicit declaration wins | +No workflow is registered at boot — retrieval agents are compiled when a +user agent that declares them starts. --- -## 4. Runtime delegation — the nested agent dispatch - -```mermaid -sequenceDiagram - autonumber - participant MLM as Main agent LLM - participant Enrich as enrich INLINE
    (JS dispatch table) - participant FORK as FORK_JOIN_DYNAMIC - participant OA as _ocg_agent
    (SUB_WORKFLOW) - participant OLM as OCG sub-agent LLM - participant Enrich2 as nested enrich INLINE - participant ORT as OcgRequestTask - participant OCG as OCG service
    (HTTPS) - - activate MLM - MLM->>MLM: sees ocg_agent in tool spec list
    decides to delegate - MLM->>+Enrich: toolCalls=[{name:"ocg_agent",args:{...}}] - Enrich->>Enrich: agentToolCfg["ocg_agent"]
    → {workflowName:"_ocg_agent"} - Enrich->>+FORK: dynamicTasks=[{type:"SUB_WORKFLOW",
    name:"_ocg_agent", ...}] - FORK->>+OA: dispatch child workflow - - loop until no more tool calls - OA->>+OLM: LLM_CHAT_COMPLETE
    (OCG system prompt with today's date
    + 7 ocg_* tools) - OLM-->>-OA: toolCalls=[{name:"ocg_query",
    args:{query, max_results, …}}] - OA->>+Enrich2: enrich runs again
    (this workflow's dispatch table) - Enrich2->>Enrich2: ocgCfg["ocg_query"]
    → {taskType:"OCG_QUERY"} - Enrich2->>+ORT: OCG_QUERY system task - deactivate Enrich2 - ORT->>+OCG: POST /api/v1/agent/query
    Authorization: Bearer - OCG-->>-ORT: raw JSON citations - ORT->>ORT: project fields, cap to responseCapChars - ORT-->>-OA: result (≤ cap) - end - - OA-->>-FORK: synthesized prose answer - FORK-->>-Enrich: child workflow output - Enrich-->>-MLM: tool result (as if ocg_agent were a function) - MLM->>MLM: continues conversation with answer
    as the latest tool result - deactivate MLM -``` - -The same compiled-workflow shape (LLM → enrich → fork → join → loop) runs -at **both** levels — the outer dispatches `SUB_WORKFLOW`, the inner -dispatches `OCG_QUERY` and friends. That's because `_ocg_agent` is just -another `AgentConfig` compiled through the same `AgentCompiler.compile()` -pipeline that produced the user's agent. - ---- +## How a call executes -## 5. The seven OCG operations +``` +main agent LLM ── tool call ──▶ SUB_WORKFLOW (retriever) + │ retriever LLM picks e.g. ocg_query + ▼ + enrich script: t.type = OCG_QUERY, + merges __ocg_url / __ocg_auth from the + tool's compiled instance binding + ▼ + OcgRequestTask (system task) + 1. target = __ocg_url (required) + 2. auth = __ocg_auth (placeholder resolved + via credential store), absent = no auth + 3. strip reserved inputs, build HTTP request + 4. send → project fields → cap chars + ▼ + OCG instance ─── citations ──▶ retriever LLM +``` -All endpoints sit under `${agentspan.ocg.url}/api/v1`. Each is backed by -a strategy class implementing `OcgOperation` (under -`runtime/ocg/operation/`); `OcgRequestTask` is a thin orchestrator that -delegates URL/method/body/projection to the strategy. +Key properties: + +- **Per-call instance binding.** The tool's binding (compiled into the + workflow as the reserved `__ocg_url`/`__ocg_auth` task inputs) is the + only instance. A tool without one is rejected at agent *start* + (`OcgToolValidator`); the task-level check is the backstop. +- **Secrets stay server-side.** `credential="OCG_US_KEY"` compiles to a + placeholder (`#{OCG_US_KEY}` standalone, `${workflow.secrets.OCG_US_KEY}` + embedded). Standalone resolution goes through the credential store scoped + by the execution token (same contract as HTTP tool headers); resolved + values are never written back to the task model. An unresolvable + placeholder fails the task rather than being sent as a bearer token. +- **Response hygiene.** Each operation projects the raw OCG response down + to the fields the LLM needs (e.g. citations), then caps it at + `response-cap-chars` *before* it is persisted or enters the LLM context. + +## The seven OCG operations + +All endpoints sit under `/api/v1`. Each is backed by a +strategy class implementing `OcgOperation` (under `runtime/ocg/operation/`); +`OcgRequestTask` is a thin orchestrator that resolves the target instance +and delegates URL/method/body/projection to the strategy. | Tool name (LLM-visible) | System task type | Endpoint | Method | | ----------------------- | --------------------- | ---------------------------------------- | -------- | @@ -282,64 +174,33 @@ delegates URL/method/body/projection to the strategy. | `ocg_memory_reinforce` | `OCG_MEMORY_REINFORCE`| `/api/v1/memories/{key}/reinforce` | `POST` | | `ocg_memory_delete` | `OCG_MEMORY_DELETE` | `/api/v1/memories/{key}` | `DELETE` | ---- - -## 6. Why `@ConditionalOnExpression` instead of `@ConditionalOnProperty` - -The OCG `@Component`s use: - -```java -@ConditionalOnExpression("'${agentspan.ocg.url:}'.length() > 0") -``` - -rather than the more obvious `@ConditionalOnProperty(name = "url")` -because Spring's default for the latter is *"present and not equal to -false"* — an empty string satisfies that and would load every OCG bean -even with `OCG_URL` unset. The expression form requires a non-empty -value, which matches the intent. +The registered TaskDef names are the lowercased task types (`ocg_query`, +…) — Conductor resolves dynamically forked tasks by name, and the dispatch +script names each task `taskType.toLowerCase()`. --- -## 7. Adding a new server-side sub-agent - -Drop one `@Component`. That's it. +## Migration from auto-expose -```java -@Component -@ConditionalOnExpression("'${agentspan.myfeature.url:}'.length() > 0") -@RequiredArgsConstructor -public class MyRegisteredAgent implements RegisteredAgent { +Prior to 2026-06-12, setting `OCG_URL` registered a `_ocg_agent` workflow at +boot and **silently appended** an `ocg_agent` tool to every compiled agent. +That behavior is removed with no flag and no shim: - private final MyFeatureProperties properties; +| Before | After | +| --- | --- | +| Every agent got `ocg_agent` for free when `OCG_URL` was set | Each agent opts in: `tools=[agent_tool(ocg_agent(model=...))]` | +| `_ocg_agent` workflow registered at boot | No boot-time workflow; retriever compiles when the declaring agent starts | +| `OCG_MODEL` required, boot failed fast without it | Model is a required SDK parameter; `OCG_MODEL` is ignored | +| One server-wide OCG instance | Per-tool `url=`/`credential=` — required; no server-side instance config at all | +| OCG fully off unless `OCG_URL` set | Execution layer gated by `agentspan.ocg.enabled`; instanceless OCG tools rejected at start | - @Override - public AgentConfig agentConfig() { - return MyAgentFactory.build(properties); - } +Anything that referenced the `_ocg_agent` workflow by name breaks; declare +the agent from the SDK instead. - @Override - public ExposeAsTool autoExpose() { - return new ExposeAsTool( - "my_agent", - "Use this when …"); - } -} -``` - -If your agent has primitive system tasks that need TaskDef entries -(most pure-LLM sub-agents won't), add one more: - -```java -@Component -@ConditionalOnExpression("'${agentspan.myfeature.url:}'.length() > 0") -public class MyRegisteredTaskDefs implements RegisteredTaskDefs { - @Override - public List taskDefs() { - return List.of(/* … */); - } -} -``` +## Non-SDK clients -**No `AgentCompiler` edit. No `AgentService` edit. No -`@PostConstruct registerWorkflow()`. No per-feature service class.** The -generic registrars handle the rest. +REST/UI clients inline the equivalent agent JSON: an `agent_tool` whose +`config.agentConfig` carries the retrieval agent — `tools` entries with +`toolType: "ocg_query"` … `"ocg_memory_delete"`, each optionally with +`config: {"url": ..., "credential": ...}`. The canonical prompt is exported +as `agentspan.agents.ocg.OCG_SYSTEM_PROMPT`. diff --git a/docs/python-sdk/api-reference.md b/docs/python-sdk/api-reference.md index cd972b3cd..cbeaf9b71 100644 --- a/docs/python-sdk/api-reference.md +++ b/docs/python-sdk/api-reference.md @@ -252,6 +252,71 @@ github = mcp_tool( ) ``` +### ocg_agent() / ocg_tools() — OCG Retrieval Sub-Agent + +OCG (Open Context Graph) is a retrieval engine over a knowledge graph of +entities (messages, channels, people, code). The SDK is the canonical home of +the OCG retrieval agent — the server provides only the execution layer (the +seven `OCG_*` system tasks that make the HTTP calls, resolve credentials, and +cap responses). OCG is **opt-in per agent**: nothing is auto-injected. + +```python +from agentspan.agents import Agent, agent_tool +from agentspan.agents.ocg import ocg_agent + +retriever = ocg_agent(model="openai/gpt-4o-mini", + url="https://ocg.example.com", credential="OCG_KEY") +main = Agent( + name="support", + model="openai/gpt-4o", + tools=[agent_tool(retriever)], # main agent delegates retrieval + instructions="...", +) +``` + +Multi-instance (data residency / multi-tenancy) — bind each retriever to its +own OCG instance: + +```python +us = ocg_agent(name="ocg_us", model="openai/gpt-4o-mini", + url="https://us.ocg.example.com", credential="OCG_US_KEY") +ca = ocg_agent(name="ocg_canada", model="openai/gpt-4o-mini", + url="https://ca.ocg.example.com", credential="OCG_CA_KEY") + +router = Agent(name="na_support", model="openai/gpt-4o", + tools=[agent_tool(us), agent_tool(ca)], instructions="...") +``` + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `model` | str | required | LLM for the retrieval agent's own turns | +| `name` | str | `"ocg_agent"` | **Must be distinct per OCG instance** — child workflows are registered by agent name | +| `url` | str | required | OCG instance base URL — every retriever binds its own instance; there is no server-side default | +| `credential` | str | None | Credential-store entry holding the OCG bearer token. Resolved server-side at execution — the secret never appears in Python code or serialized configs. Requires `url` | +| `instructions` | str | canned `OCG_SYSTEM_PROMPT` | Override the retrieval prompt | +| `max_turns` | int | 10 | Retrieval loop budget | +| `query` / `entities` / `code_history` / `memory` | bool | True | Tool subset switches | + +For a fully custom retrieval agent, take the raw tools instead: + +```python +from agentspan.agents.ocg import ocg_tools + +my_retriever = Agent( + name="retriever", + model="anthropic/claude-haiku-4-5", + instructions="My custom retrieval prompt...", + tools=ocg_tools(url="https://us.ocg.example.com", + credential="OCG_US_KEY", + memory=False), # retrieval-only subset +) +``` + +Non-SDK clients (raw REST, UI) can inline the equivalent agent JSON: an +`agent_tool` whose `agentConfig` carries `tools` entries with +`toolType: "ocg_query"` … `"ocg_memory_delete"` and (optionally) +`config: {"url": ..., "credential": ...}` per tool. + ### Mixing Tool Types Agents can use Python tools, HTTP tools, and MCP tools together: diff --git a/docs/release-notes/2026-06-12-ocg-sdk-subagent.md b/docs/release-notes/2026-06-12-ocg-sdk-subagent.md new file mode 100644 index 000000000..2dafeada2 --- /dev/null +++ b/docs/release-notes/2026-06-12-ocg-sdk-subagent.md @@ -0,0 +1,71 @@ +# Release Note — OCG Becomes an SDK-Declared Sub-Agent (Breaking) + +**Date:** 2026-06-12 + +OCG (Open Context Graph) retrieval is now **declared in user code via the +Python SDK** and supports **per-agent instance binding** (multi-tenancy / +data residency). The server keeps only the execution layer. + +## Breaking changes + +1. **Auto-expose is removed.** Setting `OCG_URL` no longer injects an + `ocg_agent` tool into every agent. Agents that need retrieval must opt + in: + + ```python + from agentspan.agents import agent_tool + from agentspan.agents.ocg import ocg_agent + + tools=[agent_tool(ocg_agent(model="openai/gpt-4o-mini"))] + ``` + +2. **The `_ocg_agent` workflow is no longer registered at boot.** Anything + referencing it by name breaks; retrieval agents compile when the + declaring agent starts. + +3. **`OCG_MODEL` / `agentspan.ocg.model` is removed and ignored.** The + retrieval agent's model is a required SDK parameter. The boot-time + fail-fast tied to it is gone. + +4. **Gating changed; server-side instance config removed.** The `OCG_*` + system tasks are registered when `agentspan.ocg.enabled=true` (the + default) — no longer conditional on `OCG_URL`. `agentspan.ocg.url` / + `api-key` are **gone**: every OCG tool binds its own instance + (`url=`, required) from the SDK; there is no server-wide default. + +## New + +- `agentspan.agents.ocg` — `ocg_agent()` (prebuilt retrieval agent), + `ocg_tools()` (raw tool defs, subset switches), `OCG_SYSTEM_PROMPT`. +- Per-tool instance binding: `ocg_agent(url=..., credential=...)` — `url` + is required; the credential is a credential-store *name*, resolved + server-side at execution; secrets never enter Python code or workflow + definitions. +- Start-time validation: an OCG tool without a bound `url` is rejected at + agent start (not mid-conversation), as is any OCG tool when + `agentspan.ocg.enabled=false`. +- Every compiled tool spec now carries a `selfDescribing: true` marker — + top-level and inside `configParams` (the copy that survives `ToolSpec` + deserialization) — consumed by embedding hosts (orkes-conductor's + `OrkesLLM`) to pass AgentSpan tool specs to the LLM without + integration-store resolution. + +## Fixed + +- OCG TaskDefs are now registered under the names the dispatch script + actually schedules (`ocg_query`, …, `ocg_memory_delete`). Previously they + were registered under the operation labels (`query`, `memory_set`, …), + which failed dynamic-fork dispatch with *"Cannot find task by name + ocg_query in the task definitions."* + +## Migration + +| If you relied on… | Do this instead | +| --- | --- | +| Auto-injected `ocg_agent` on every agent | Add `agent_tool(ocg_agent(model=...))` to each agent that needs retrieval | +| `OCG_MODEL` env var | Pass `model=` to `ocg_agent()` | +| The boot-registered `_ocg_agent` workflow | Declare the retriever from the SDK | +| Server-wide `OCG_URL`/`OCG_API_KEY` | Bind per tool: `ocg_agent(url=..., credential=...)` | + +Details: `docs/ocg-agent-flow.md` and +`docs/design/2026-06-12-ocg-sdk-subagent-design.md`. diff --git a/e2e/ocg/jira_ocg_smoke.py b/e2e/ocg/jira_ocg_smoke.py new file mode 100644 index 000000000..26daaf9e1 --- /dev/null +++ b/e2e/ocg/jira_ocg_smoke.py @@ -0,0 +1,60 @@ +"""Jira-over-OCG smoke check. + +OCG is now opt-in from the SDK (auto-expose is gone), so the agent must +declare its retrieval tooling explicitly. This smoke uses the sub-agent +shape; both shapes live as SDK examples: + + - sdk/python/examples/116_ocg_subagent.py (delegate to ocg_agent()) + - sdk/python/examples/117_ocg_direct_tools.py (main agent calls ocg_query) + +Run (from sdk/python, against the embedded orkes server on 8080):: + + OCG_URL=https://test.contextgraph.io \ + OCG_CREDENTIAL=OCG_PUBLIC_KEY \ + uv run python ../../e2e/ocg/jira_ocg_smoke.py + +OCG_CREDENTIAL names a secret in the server's secrets store holding the +instance's bearer token (store it once, e.g. orkes UI -> Secrets, or +PUT /api/secrets/OCG_PUBLIC_KEY). The token itself never appears here. +""" + +import os + +from agentspan.agents import Agent, AgentRuntime, agent_tool +from agentspan.agents.ocg import ocg_agent + +# The agentspan runtime is embedded in the Conductor server, which listens +# on 8080 (not the standalone default 6767). Override via env if needed. +SERVER_URL = os.environ.get("AGENTSPAN_SERVER_URL", "http://localhost:8080/api") +MODEL = os.environ.get("AGENTSPAN_LLM_MODEL", "openai/gpt-4o-mini") + +# Every OCG tool binds the instance it talks to — there is no server-side +# default. OCG_CREDENTIAL optionally names a credential-store entry for the +# instance's bearer token (for unauthenticated/local instances, leave unset). +OCG_URL = os.environ.get("OCG_URL") or "" +OCG_CREDENTIAL = os.environ.get("OCG_CREDENTIAL") +if not OCG_URL: + raise SystemExit("Set OCG_URL to your OCG instance, e.g. https://test.contextgraph.io") + +prompt = ( + "Catch me up on 'Improvements to Python SDK -- performance, Feature " + "parity, logging, metrics etc'. What's the current state, what's " + "underneath it, and what's been changing in the codebase?" +) + +retriever = ocg_agent(name="ocg_retriever", model=MODEL, url=OCG_URL, credential=OCG_CREDENTIAL) + +agent = Agent( + name="jira_ocg_smoke", + model=MODEL, + instructions=( + "You answer questions about the team's work. Delegate every lookup " + "to your retrieval tool and synthesize its cited answer." + ), + tools=[agent_tool(retriever)], +) + +if __name__ == "__main__": + with AgentRuntime(server_url=SERVER_URL) as runtime: + result = runtime.run(agent, prompt) + result.print_result() diff --git a/e2e/ocg/requirements.txt b/e2e/ocg/requirements.txt new file mode 100644 index 000000000..fdd2fa2d3 --- /dev/null +++ b/e2e/ocg/requirements.txt @@ -0,0 +1,5 @@ +# From this directory: +# python -m venv venv +# source venv/bin/activate +# pip install -e ../../sdk/python +# No extra deps needed — OCG_QUERY is a server-side system task. diff --git a/sdk/python/e2e/test_suite22_ocg.py b/sdk/python/e2e/test_suite22_ocg.py new file mode 100644 index 000000000..645321fab --- /dev/null +++ b/sdk/python/e2e/test_suite22_ocg.py @@ -0,0 +1,200 @@ +"""Suite 22: OCG multi-instance — per-tool instance binding isolation. + +The multi-tenancy guarantee of the SDK-defined OCG design: two retrieval +agents bound to two different OCG instances (`ocg_agent(url=...)`) each hit +their own instance and ONLY that instance. Validation is purely structural — +recorded HTTP traffic on the stubs — never LLM-judged output quality. + + 1. US agent (agent_tool → ocg_agent bound to stub A) → traffic on A, none on B + 2. Canada agent (bound to stub B) → traffic on B, none on A + 3. Negative: agent with no OCG tools → no traffic on either stub + +Manages two stub OCG instances on dedicated ports. +No mocks of agentspan itself. Real server, real LLM, stub OCG backends. +""" + +import json +import os +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +import pytest + +from agentspan.agents import Agent, agent_tool +from agentspan.agents.ocg import ocg_agent + +pytestmark = [ + pytest.mark.e2e, + pytest.mark.xdist_group("ocg"), +] + +# ── Configuration ──────────────────────────────────────────────────────── + +US_PORT = 3061 +CA_PORT = 3062 +TIMEOUT = 120 + +MODEL = os.environ.get("AGENTSPAN_LLM_MODEL", "openai/gpt-4o-mini") + +# The agentspan server resolves the per-tool OCG URL server-side, so the +# stubs must be reachable from the server process — localhost works for the +# local e2e topology (server and tests on the same host). +US_URL = f"http://localhost:{US_PORT}" +CA_URL = f"http://localhost:{CA_PORT}" + + +# ── Stub OCG instance ──────────────────────────────────────────────────── + + +class _StubOcg: + """Minimal OCG lookalike: answers /api/v1/agent/query with canned + citations and records every request it receives.""" + + def __init__(self, port: int, region: str): + self.port = port + self.region = region + self.requests: list = [] # (method, path, body) tuples + stub = self + + class Handler(BaseHTTPRequestHandler): + def _record_and_reply(self, body: str): + stub.requests.append((self.command, self.path, body)) + payload = { + "citations": [ + { + "source_item_id": f"{stub.region}-item-1", + "title": f"{stub.region} maintenance window", + "container_id": f"#{stub.region}-ops", + "snippet": f"The {stub.region} maintenance window is Saturday 02:00 UTC.", + } + ] + } + data = json.dumps(payload).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def do_POST(self): + length = int(self.headers.get("Content-Length", 0)) + self._record_and_reply(self.rfile.read(length).decode()) + + def do_GET(self): + self._record_and_reply("") + + def do_DELETE(self): + self._record_and_reply("") + + def log_message(self, *args): # silence per-request stderr noise + pass + + self._server = ThreadingHTTPServer(("0.0.0.0", port), Handler) + self._thread = threading.Thread(target=self._server.serve_forever, daemon=True) + + def start(self): + self._thread.start() + return self + + def stop(self): + self._server.shutdown() + self._server.server_close() + + @property + def query_requests(self): + return [r for r in self.requests if r[1].startswith("/api/v1/agent/query")] + + +@pytest.fixture(scope="module") +def stubs(): + us = _StubOcg(US_PORT, "us").start() + ca = _StubOcg(CA_PORT, "canada").start() + try: + yield us, ca + finally: + us.stop() + ca.stop() + + +def _retrieval_main(name: str, retriever) -> Agent: + return Agent( + name=name, + model=MODEL, + instructions=( + "You answer operational questions. You MUST call your retrieval " + "tool to look up the answer before responding — never answer " + "from memory and never ask the user clarifying questions. Pass " + "the user's question to the retrieval tool verbatim." + ), + tools=[agent_tool(retriever)], + max_turns=6, + ) + + +PROMPT = ( + "Search for recent messages about the maintenance window for cluster " + "prod-east and report exactly what the messages say. Do not ask " + "clarifying questions — search first." +) + + +# ── Tests ──────────────────────────────────────────────────────────────── + + +@pytest.mark.timeout(TIMEOUT * 2) +def test_us_agent_hits_only_us_instance(runtime, stubs): + us, ca = stubs + us_before, ca_before = len(us.query_requests), len(ca.query_requests) + + retriever = ocg_agent(name="ocg_us_e2e", model=MODEL, url=US_URL) + main = _retrieval_main("ocg_e2e_us_main", retriever) + + result = runtime.run(main, PROMPT, timeout=TIMEOUT) + assert result is not None + + # The multi-tenancy guarantee, asserted on recorded traffic: + assert len(us.query_requests) > us_before, ( + f"US-bound retriever never queried the US OCG stub — stub saw: {us.requests}" + ) + assert len(ca.query_requests) == ca_before, ( + f"US-bound retriever leaked traffic to the Canada stub: {ca.requests}" + ) + + +@pytest.mark.timeout(TIMEOUT * 2) +def test_canada_agent_hits_only_canada_instance(runtime, stubs): + us, ca = stubs + us_before, ca_before = len(us.query_requests), len(ca.query_requests) + + retriever = ocg_agent(name="ocg_ca_e2e", model=MODEL, url=CA_URL) + main = _retrieval_main("ocg_e2e_ca_main", retriever) + + result = runtime.run(main, PROMPT, timeout=TIMEOUT) + assert result is not None + + assert len(ca.query_requests) > ca_before, ( + f"Canada-bound retriever never queried the Canada OCG stub — stub saw: {ca.requests}" + ) + assert len(us.query_requests) == us_before, ( + f"Canada-bound retriever leaked traffic to the US stub: {us.requests}" + ) + + +@pytest.mark.timeout(TIMEOUT * 2) +def test_agent_without_ocg_tools_generates_no_ocg_traffic(runtime, stubs): + us, ca = stubs + us_before, ca_before = len(us.requests), len(ca.requests) + + plain = Agent( + name="ocg_e2e_plain", + model=MODEL, + instructions="Answer briefly from your own knowledge.", + max_turns=2, + ) + + result = runtime.run(plain, "Say hello in one word.", timeout=TIMEOUT) + assert result is not None + + # Inverse of the deleted auto-expose behavior: no OCG opt-in, no OCG calls. + assert len(us.requests) == us_before + assert len(ca.requests) == ca_before diff --git a/sdk/python/examples/116_ocg_subagent.py b/sdk/python/examples/116_ocg_subagent.py new file mode 100644 index 000000000..e020e2cd0 --- /dev/null +++ b/sdk/python/examples/116_ocg_subagent.py @@ -0,0 +1,88 @@ +# Copyright (c) 2025 Agentspan +# Licensed under the MIT License. + +"""116 — OCG retrieval via the prebuilt sub-agent. + +The main agent delegates retrieval to an OCG (Open Context Graph) +sub-agent: ``ocg_agent()`` returns an ordinary ``Agent`` carrying the +canned retrieval prompt and all seven ``ocg_*`` tools; wrapping it with +``agent_tool()`` exposes it to the main agent's LLM as a single tool. + +When the main agent calls it, the sub-agent runs its *own* LLM loop — +it can issue several OCG queries, walk entity neighborhoods, pull code +history — and returns one synthesized, cited answer. The main agent's +context only ever sees that final answer, not the raw graph payloads. + +Choose this shape when retrieval takes judgment (multi-step lookups, +aggregation in two steps, query reformulation). For a single direct +lookup from the main agent's own loop, see +``117_ocg_direct_tools.py``. + +OCG is opt-in per agent — nothing is auto-injected, and every OCG tool +binds the instance it talks to (no server-side default): set +``OCG_INSTANCE_URL`` (and optionally ``OCG_CREDENTIAL``, a +credential-store *name*). + +Run (from ``sdk/python``):: + + # one-time: store the OCG bearer token in the server's secrets store, + # e.g. in orkes: PUT /api/secrets/OCG_PUBLIC_KEY '""' + + OCG_INSTANCE_URL=https://test.contextgraph.io \ + OCG_CREDENTIAL=OCG_PUBLIC_KEY \ + uv run python examples/116_ocg_subagent.py + + # against an embedded server (e.g. orkes on 8080), add: + # AGENTSPAN_SERVER_URL=http://localhost:8080/api +""" + +import os + +from agentspan.agents import Agent, AgentRuntime, agent_tool +from agentspan.agents.ocg import ocg_agent + +MODEL = os.environ.get("AGENTSPAN_LLM_MODEL", "openai/gpt-4o-mini") + +# Per-tool instance binding — required: every OCG tool binds the instance +# it talks to; there is no server-side default. +OCG_INSTANCE_URL = os.environ.get("OCG_INSTANCE_URL") or "" +OCG_CREDENTIAL = os.environ.get("OCG_CREDENTIAL") # credential-store name, never the key +if not OCG_INSTANCE_URL: + raise SystemExit( + "Set OCG_INSTANCE_URL to your OCG instance, e.g. https://test.contextgraph.io" + ) + +PROMPT = ( + "Catch me up on 'Improvements to Python SDK -- performance, Feature " + "parity, logging, metrics etc'. What's the current state, what's " + "underneath it, and what's been changing in the codebase?" +) + + +def main() -> None: + retriever = ocg_agent( + name="ocg_retriever", + model=MODEL, + url=OCG_INSTANCE_URL, + credential=OCG_CREDENTIAL, + ) + + main_agent = Agent( + name="jira_ocg_subagent", + model=MODEL, + instructions=( + "You answer questions about the team's work. Delegate every " + "lookup to your retrieval tool — messages, Jira tickets, and " + "code history all live behind it. Synthesize what it returns " + "into a concise brief and keep its citations." + ), + tools=[agent_tool(retriever)], + ) + + with AgentRuntime() as runtime: + result = runtime.run(main_agent, PROMPT) + result.print_result() + + +if __name__ == "__main__": + main() diff --git a/sdk/python/examples/117_ocg_direct_tools.py b/sdk/python/examples/117_ocg_direct_tools.py new file mode 100644 index 000000000..48b0d0ee4 --- /dev/null +++ b/sdk/python/examples/117_ocg_direct_tools.py @@ -0,0 +1,88 @@ +# Copyright (c) 2025 Agentspan +# Licensed under the MIT License. + +"""117 — OCG retrieval via a direct tool call (no sub-agent). + +The main agent holds the OCG query tool *itself*: ``ocg_tools()`` +returns raw ``ToolDef``s that dispatch straight to the server's +``OCG_*`` system tasks, so the main agent's own LLM issues the query +and reads the citations — no sub-agent hop, no second LLM loop. + +Compared to ``116_ocg_subagent.py``: + +- one LLM round-trip cheaper per lookup — there is no retrieval agent + spending its own turns; +- the raw (projected, capped) OCG response lands directly in the main + agent's context, so IT does the reading — fine for a single focused + query, wasteful when retrieval takes several exploratory calls; +- you own the retrieval prompting: the canned OCG system prompt is the + sub-agent's, so any query-writing guidance the model needs (specific + keywords, time bounds, two-step aggregation) belongs in your own + ``instructions`` here. + +This example exposes only ``ocg_query`` (the subset switches turn off +entity/code/memory tools) — the narrowest possible OCG surface. + +Instance binding works exactly as in 116: ``OCG_INSTANCE_URL`` (required) / +``OCG_CREDENTIAL`` env vars. + +Run (from ``sdk/python``):: + + OCG_INSTANCE_URL=https://test.contextgraph.io \ + OCG_CREDENTIAL=OCG_PUBLIC_KEY \ + uv run python examples/117_ocg_direct_tools.py + + # against an embedded server (e.g. orkes on 8080), add: + # AGENTSPAN_SERVER_URL=http://localhost:8080/api +""" + +import os + +from agentspan.agents import Agent, AgentRuntime +from agentspan.agents.ocg import ocg_tools + +MODEL = os.environ.get("AGENTSPAN_LLM_MODEL", "openai/gpt-4o-mini") + +OCG_INSTANCE_URL = os.environ.get("OCG_INSTANCE_URL") or "" +OCG_CREDENTIAL = os.environ.get("OCG_CREDENTIAL") # credential-store name, never the key +if not OCG_INSTANCE_URL: + raise SystemExit( + "Set OCG_INSTANCE_URL to your OCG instance, e.g. https://test.contextgraph.io" + ) + +PROMPT = ( + "Catch me up on 'Improvements to Python SDK -- performance, Feature " + "parity, logging, metrics etc'. What's the current state, what's " + "underneath it, and what's been changing in the codebase?" +) + + +def main() -> None: + main_agent = Agent( + name="jira_ocg_direct", + model=MODEL, + instructions=( + "You answer questions about the team's work using ocg_query, " + "a retrieval tool over a knowledge graph of messages, Jira " + "tickets, and code. Query with specific keywords (ticket " + "titles, component names) — under ~15 content words. If the " + "question spans topics, issue one query per topic, then " + "synthesize the citations into a concise brief." + ), + tools=ocg_tools( + url=OCG_INSTANCE_URL, + credential=OCG_CREDENTIAL, + query=True, + entities=False, + code_history=False, + memory=False, + ), + ) + + with AgentRuntime() as runtime: + result = runtime.run(main_agent, PROMPT) + result.print_result() + + +if __name__ == "__main__": + main() diff --git a/sdk/python/src/agentspan/agents/__init__.py b/sdk/python/src/agentspan/agents/__init__.py index 1244e9eee..fb9f56792 100644 --- a/sdk/python/src/agentspan/agents/__init__.py +++ b/sdk/python/src/agentspan/agents/__init__.py @@ -224,6 +224,9 @@ def resolve_credentials(input_data: dict, names: list) -> dict: wait_for_message_tool, ) +# OCG (Open Context Graph) retrieval sub-agent +from agentspan.agents.ocg import OCG_SYSTEM_PROMPT, ocg_agent, ocg_tools + # openai-agents name alias — ``from agentspan.agents import function_tool`` function_tool = tool @@ -255,6 +258,10 @@ def resolve_credentials(input_data: dict, names: list) -> dict: "agent_tool", "api_tool", "http_tool", + # OCG retrieval sub-agent + "OCG_SYSTEM_PROMPT", + "ocg_agent", + "ocg_tools", "human_tool", "mcp_tool", "wait_for_message_tool", diff --git a/sdk/python/src/agentspan/agents/ocg.py b/sdk/python/src/agentspan/agents/ocg.py new file mode 100644 index 000000000..a931d15f3 --- /dev/null +++ b/sdk/python/src/agentspan/agents/ocg.py @@ -0,0 +1,395 @@ +# Copyright (c) 2025 Agentspan +# Licensed under the MIT License. See LICENSE file in the project root for details. + +"""OCG (Open Context Graph) retrieval sub-agent. + +OCG is a retrieval engine over a knowledge graph of entities (messages, +channels, people, code) linked by claims and relationships. This module is +the canonical definition of the OCG retrieval agent — system prompt, tool +schemas, and instance binding all live here; the server provides only the +execution layer (the seven ``OCG_*`` system tasks that make the HTTP calls, +resolve credentials, and cap responses). + +Typical usage — delegate retrieval from a main agent:: + + from agentspan.agents import Agent, agent_tool + from agentspan.agents.ocg import ocg_agent + + retriever = ocg_agent(model="openai/gpt-4o-mini", + url="https://ocg.example.com", + credential="OCG_KEY") + main = Agent(name="support", model="openai/gpt-4o", + tools=[agent_tool(retriever)], instructions="...") + +Multi-instance (e.g. data residency) — bind each retriever to its own OCG:: + + us = ocg_agent(name="ocg_us", model="openai/gpt-4o-mini", + url="https://us.ocg.example.com", credential="OCG_US_KEY") + ca = ocg_agent(name="ocg_canada", model="openai/gpt-4o-mini", + url="https://ca.ocg.example.com", credential="OCG_CA_KEY") + +``url`` is required — every OCG tool set binds the instance it talks to; +there is no server-side default. ``credential`` names an entry in the +server's credential store — the secret itself never appears in Python code +or serialized configs. + +.. warning:: + Agents bound to **different** OCG instances must have **distinct** + ``name``s: inline ``agent_tool`` child workflows are registered by agent + name, so two differently-configured agents sharing a name overwrite each + other's workflow definition. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +from agentspan.agents.tool import ToolDef + +if TYPE_CHECKING: + from agentspan.agents.agent import Agent + +# ── System prompt ─────────────────────────────────────────────────────── +# +# ``${workflow.input.__today__}`` is substituted by Conductor when the LLM +# task is scheduled — the agent_tool dispatch script injects ``__today__`` +# with the current UTC date on every call, so relative-date queries anchor +# on the real current date instead of one baked in at definition time. + +OCG_SYSTEM_PROMPT = """\ +Today's date is ${workflow.input.__today__} (UTC). When a user asks for +"recent" / "last week" / any relative range, anchor on this date. +Never invent a date range — if no range is implied by the user, omit +start_time/end_time from the request. +If the range extends to the present ("recent", "current state", +"catch me up", "last N days"), set start_time and OMIT end_time — +the results then run through now. Only set end_time when the user +asks about a window that closed in the past. Never end a range at a +month boundary before today; that silently drops the newest data. + +You are querying an OCG (Observability Context Graph). It is a RETRIEVAL +engine over a knowledge graph of entities (messages, channels, people) +linked by claims and relationships. It is NOT an aggregation engine. + +It can answer: + - "Find messages in channel X about Y" + - "Show TIMED_OUT errors for cluster " + - "What entities mention 'health check failure'?" + - "Recent messages in #cloud_saas_health_check_alerts" + +It CANNOT directly answer (you must do it yourself in two steps): + - "How many of X are there?" / "Which X is most frequent?" + - "Group these by Y" / "Top N by count" + - Statistical or comparative questions + +For aggregation questions, use a TWO-STEP pattern: + 1. RETRIEVE: ask OCG for the raw set of relevant entities. + - Use specific terms (cluster names, error codes, channel names). + - Use start_time (and end_time only for windows closed in the + past) to bound the range. + - Set max_results high (e.g. 500) so you get the full set, not a + top-N sample. + - Avoid hedging words ("frequently", "across", "occurrences") — + OCG ranks by keyword presence, and these are noise tokens. + 2. AGGREGATE: count, group, rank yourself from the citation list. + +Query length: keep it under ~15 content words. Long prompts dilute the +BM25 keyword set; OCG's parser is extracting things like "happen", +"identify", "top one" which are not real signal. + +Bad: "Across all clusters, what alert/notification/error type appears + most frequently? Group similar alerts and tell me which one has + the highest count and how many clusters it affected." + +Good (step 1): { + "query": "TIMED_OUT health check failure cluster", + "max_results": 500, + "start_time": "T00:00:00Z" +} +(end_time omitted — the range runs through now.) +Then parse the returned citations, extract cluster names from titles, +build the frequency table in your reasoning.""" + + +# ── JSON-schema helpers ───────────────────────────────────────────────── + + +def _prop(type_: str, description: str, default: Any = None) -> Dict[str, Any]: + schema: Dict[str, Any] = {"type": type_, "description": description} + if default is not None: + schema["default"] = default + return schema + + +def _array(item_type: str, description: str) -> Dict[str, Any]: + return {"type": "array", "description": description, "items": {"type": item_type}} + + +def _object(properties: Dict[str, Any], required: List[str]) -> Dict[str, Any]: + schema: Dict[str, Any] = {"type": "object", "properties": properties} + if required: + schema["required"] = required + return schema + + +# ── Tool definitions ──────────────────────────────────────────────────── + + +def _query_tool() -> Dict[str, Any]: + return { + "name": "ocg_query", + "description": ( + "Query the Open Context Graph for structured retrieval. " + "Returns citations (source_item_id, title, container_id, snippet) " + "and traversal_results when traversal_level > 0." + ), + "schema": _object( + { + "query": _prop("string", "Natural-language retrieval query."), + "max_results": _prop("integer", "Max citations to return.", 10), + "traversal_level": _prop( + "integer", "0 = citations only, 1 = neighborhood, 2-3 = multi-hop.", 1 + ), + "start_time": _prop("string", "ISO-8601 lower bound (inclusive). Optional."), + "end_time": _prop("string", "ISO-8601 upper bound (exclusive). Optional."), + }, + ["query"], + ), + } + + +def _get_entity_tool() -> Dict[str, Any]: + return { + "name": "ocg_get_entity", + "description": "Fetch one entity by its canonical id.", + "schema": _object( + {"entity_id": _prop("string", "Canonical entity id from an ocg_query result row.")}, + ["entity_id"], + ), + } + + +def _neighborhood_tool() -> Dict[str, Any]: + return { + "name": "ocg_neighborhood", + "description": ( + "Get an entity plus its graph neighbors out to `depth` hops. " + "Use limit <= 10, depth=1 on the first call — well-connected " + "entities can have many edges and large responses will be truncated." + ), + "schema": _object( + { + "entity_id": _prop("string", "Entity at the center of the neighborhood."), + "depth": _prop("integer", "Hop depth (use depth=1 on first call).", 2), + "limit": _prop( + "integer", "Cap on neighbors returned (use <= 10 on first call).", 50 + ), + }, + ["entity_id"], + ), + } + + +def _code_history_tool() -> Dict[str, Any]: + return { + "name": "ocg_code_history", + "description": "Last N commits that touched a file in an ingested repo.", + "schema": _object( + { + "repo_id": _prop("string", "Ingested repository id."), + "path": _prop("string", "Path within the repo."), + "limit": _prop("integer", "Max commits to return.", 20), + }, + ["repo_id", "path"], + ), + } + + +def _memory_set_tool() -> Dict[str, Any]: + return { + "name": "ocg_memory_set", + "description": ( + "Create or overwrite a memory in OCG. Cap inferred confidence at 0.7; " + "never write PII or secrets." + ), + "schema": _object( + { + "key": _prop("string", "Memory key."), + "agent": _prop("string", 'Agent owner (e.g. "agent:").'), + "user": _prop("string", 'User owner (e.g. "user:").'), + "string_value": _prop("string", "Stored value."), + "description": _prop("string", "Human-readable description."), + "scope": _prop( + "string", + "Memory scope. One of MEMORY_SCOPE_SESSION, MEMORY_SCOPE_AGENT, " + "MEMORY_SCOPE_USER, MEMORY_SCOPE_SHARED, MEMORY_SCOPE_GLOBAL.", + "MEMORY_SCOPE_USER", + ), + "confidence": _prop("number", "Inferred confidence in [0,1]. Cap at 0.7.", 0.7), + "source_ref": _prop("string", "Free-form source reference (e.g. message id)."), + "evidence_ids": _array("string", "Supporting evidence entity ids."), + "tags": _array("string", "Tags."), + "expires_at": _prop("string", "ISO-8601 expiry. Optional — default 180 days."), + "idempotency_key": _prop("string", "Idempotency key. Optional."), + }, + ["key", "agent", "user", "string_value", "description"], + ), + } + + +def _memory_reinforce_tool() -> Dict[str, Any]: + return { + "name": "ocg_memory_reinforce", + "description": ( + "Reinforce an existing memory on independent re-observation. confidence_boost must be <= 0.05." + ), + "schema": _object( + { + "key": _prop("string", "Memory key."), + "agent": _prop("string", "Agent owner."), + "user": _prop("string", "User owner."), + "confidence_boost": _prop( + "number", "Boost to add (must be <= 0.05 to prevent compounding drift).", 0.05 + ), + "source_ref": _prop("string", "Free-form source reference."), + }, + ["key", "agent", "user"], + ), + } + + +def _memory_delete_tool() -> Dict[str, Any]: + return { + "name": "ocg_memory_delete", + "description": ( + "Delete a memory by key. Prefer ocg_memory_set with a corrected value " + "over deletion (preserves history)." + ), + "schema": _object( + { + "key": _prop("string", "Memory key."), + "agent": _prop("string", "Agent owner."), + "user": _prop("string", "User owner."), + }, + ["key", "agent", "user"], + ), + } + + +# ── Public factories ──────────────────────────────────────────────────── + + +def ocg_tools( + *, + url: str, + credential: Optional[str] = None, + query: bool = True, + entities: bool = True, + code_history: bool = True, + memory: bool = True, +) -> List[ToolDef]: + """Build the raw OCG :class:`ToolDef` list for a custom retrieval agent. + + Each tool dispatches to the matching ``OCG_*`` system task on the + server, which owns the HTTP call, credential resolution, field + projection, and response capping. + + Args: + url: Base URL of the OCG instance this tool set targets. Required — + there is no server-side default instance. + credential: Name of a credential-store entry holding the OCG bearer + token. The server resolves it at execution time — the secret + never appears in the serialized config. + query: Include ``ocg_query``. + entities: Include ``ocg_get_entity`` + ``ocg_neighborhood``. + code_history: Include ``ocg_code_history``. + memory: Include ``ocg_memory_set`` / ``ocg_memory_reinforce`` / + ``ocg_memory_delete``. + + Raises: + ValueError: If ``url`` is blank. + """ + if not url or not url.strip(): + raise ValueError( + "ocg_tools() requires a non-blank url: every OCG tool set binds its own instance." + ) + + config: Dict[str, Any] = {"url": url} + credentials: List[str] = [] + if credential: + config["credential"] = credential + credentials = [credential] + + selected: List[Dict[str, Any]] = [] + if query: + selected.append(_query_tool()) + if entities: + selected.append(_get_entity_tool()) + selected.append(_neighborhood_tool()) + if code_history: + selected.append(_code_history_tool()) + if memory: + selected.append(_memory_set_tool()) + selected.append(_memory_reinforce_tool()) + selected.append(_memory_delete_tool()) + + return [ + ToolDef( + name=spec["name"], + description=spec["description"], + input_schema=spec["schema"], + tool_type=spec["name"], + config=dict(config), + credentials=list(credentials), + ) + for spec in selected + ] + + +def ocg_agent( + *, + model: str, + url: str, + name: str = "ocg_agent", + credential: Optional[str] = None, + instructions: Optional[str] = None, + max_turns: int = 10, + query: bool = True, + entities: bool = True, + code_history: bool = True, + memory: bool = True, +) -> "Agent": + """Build the prebuilt OCG retrieval :class:`Agent`. + + Returns an ordinary :class:`Agent` — wrap it with :func:`agent_tool` to + let a main agent delegate retrieval, or use it as a pipeline stage to + retrieve before the main agent runs. + + Args: + model: LLM for the retrieval agent's own turns (required — the right + model depends on cost/latency targets and the OCG corpus). + url: OCG instance base URL (required — no server-side default). + name: Agent name. **Must be distinct per OCG instance** — child + workflows are registered by agent name (see module warning). + credential: Credential-store entry for the instance's bearer token. + instructions: Override the canned :data:`OCG_SYSTEM_PROMPT`. + max_turns: Retrieval loop budget. + query / entities / code_history / memory: Tool subset switches, + forwarded to :func:`ocg_tools`. + """ + from agentspan.agents.agent import Agent + + return Agent( + name=name, + model=model, + instructions=instructions if instructions is not None else OCG_SYSTEM_PROMPT, + tools=ocg_tools( + url=url, + credential=credential, + query=query, + entities=entities, + code_history=code_history, + memory=memory, + ), + max_turns=max_turns, + ) diff --git a/sdk/python/tests/unit/test_ocg.py b/sdk/python/tests/unit/test_ocg.py new file mode 100644 index 000000000..a00c2264f --- /dev/null +++ b/sdk/python/tests/unit/test_ocg.py @@ -0,0 +1,180 @@ +# Copyright (c) 2025 Agentspan +# Licensed under the MIT License. See LICENSE file in the project root for details. + +"""Unit tests for the OCG (Open Context Graph) retrieval sub-agent factories.""" + +import pytest + +from agentspan.agents.ocg import OCG_SYSTEM_PROMPT, ocg_agent, ocg_tools +from agentspan.agents.tool import ToolDef + +ALL_TOOL_TYPES = { + "ocg_query", + "ocg_get_entity", + "ocg_neighborhood", + "ocg_code_history", + "ocg_memory_set", + "ocg_memory_reinforce", + "ocg_memory_delete", +} + + +URL = "https://us.ocg.example.com" + + +class TestOcgTools: + def test_default_returns_all_seven(self): + tools = ocg_tools(url=URL) + assert len(tools) == 7 + assert {t.tool_type for t in tools} == ALL_TOOL_TYPES + # Name matches tool_type for every OCG tool (the server keys + # ocgConfig by tool name). + assert all(t.name == t.tool_type for t in tools) + assert all(isinstance(t, ToolDef) for t in tools) + + def test_memory_false_returns_retrieval_only(self): + tools = ocg_tools(url=URL, memory=False) + assert len(tools) == 4 + assert {t.tool_type for t in tools} == { + "ocg_query", + "ocg_get_entity", + "ocg_neighborhood", + "ocg_code_history", + } + + def test_subset_switches(self): + tools = ocg_tools(url=URL, entities=False, code_history=False, memory=False) + assert [t.tool_type for t in tools] == ["ocg_query"] + + def test_url_is_required(self): + # There is no server-side default instance — every OCG tool set + # binds its own. + with pytest.raises(TypeError): + ocg_tools() + with pytest.raises(ValueError, match="url"): + ocg_tools(url="") + + def test_instance_binding_lands_in_config(self): + tools = ocg_tools(url=URL, credential="OCG_US_KEY") + for t in tools: + assert t.config["url"] == URL + assert t.config["credential"] == "OCG_US_KEY" + # Declared so the execution token bounds credential resolution + # (same wire contract as http_tool headers). + assert t.credentials == ["OCG_US_KEY"] + + def test_url_without_credential_is_allowed(self): + tools = ocg_tools(url="https://local-ocg:8080") + for t in tools: + assert t.config == {"url": "https://local-ocg:8080"} + assert t.credentials == [] + + def test_credential_without_url_raises(self): + with pytest.raises(TypeError): + ocg_tools(credential="OCG_US_KEY") + + def test_schemas_have_required_fields(self): + by_type = {t.tool_type: t for t in ocg_tools(url=URL)} + assert by_type["ocg_query"].input_schema["required"] == ["query"] + assert by_type["ocg_get_entity"].input_schema["required"] == ["entity_id"] + assert by_type["ocg_code_history"].input_schema["required"] == ["repo_id", "path"] + assert by_type["ocg_memory_set"].input_schema["required"] == [ + "key", + "agent", + "user", + "string_value", + "description", + ] + + +class TestOcgAgent: + def test_returns_plain_agent(self): + from agentspan.agents.agent import Agent + + agent = ocg_agent(model="openai/gpt-4o-mini", url=URL) + assert isinstance(agent, Agent) + assert agent.name == "ocg_agent" + assert agent.model == "openai/gpt-4o-mini" + assert agent.max_turns == 10 + + def test_model_is_required(self): + with pytest.raises(TypeError): + ocg_agent(url=URL) # no model + + def test_url_is_required(self): + with pytest.raises(TypeError): + ocg_agent(model="openai/gpt-4o-mini") # no url + + def test_canned_prompt_is_default(self): + agent = ocg_agent(model="openai/gpt-4o-mini", url=URL) + assert agent.instructions == OCG_SYSTEM_PROMPT + # Execution-time date anchor must survive into the prompt verbatim — + # Conductor substitutes it when the LLM task is scheduled. + assert "${workflow.input.__today__}" in agent.instructions + + def test_instructions_override(self): + agent = ocg_agent( + model="openai/gpt-4o-mini", url=URL, instructions="Custom retrieval prompt." + ) + assert agent.instructions == "Custom retrieval prompt." + + def test_instance_binding_flows_to_tools(self): + agent = ocg_agent( + name="ocg_us", + model="openai/gpt-4o-mini", + url="https://us.ocg.example.com", + credential="OCG_US_KEY", + ) + assert agent.name == "ocg_us" + from agentspan.agents.tool import get_tool_def + + tool_defs = [get_tool_def(t) for t in agent.tools] + assert len(tool_defs) == 7 + for td in tool_defs: + assert td.config["url"] == "https://us.ocg.example.com" + assert td.config["credential"] == "OCG_US_KEY" + + def test_tool_subset_flags_forwarded(self): + agent = ocg_agent(model="openai/gpt-4o-mini", url=URL, memory=False) + assert len(agent.tools) == 4 + + def test_exported_from_agents_package(self): + from agentspan.agents import ocg_agent as exported_agent + from agentspan.agents import ocg_tools as exported_tools + + assert exported_agent is ocg_agent + assert exported_tools is ocg_tools + + +class TestOcgWireFormat: + def test_serializes_with_instance_config(self): + """The serialized agent_tool child must carry each OCG tool's + toolType + config so ToolCompiler can bake the instance binding.""" + from agentspan.agents.agent import Agent + from agentspan.agents.config_serializer import AgentConfigSerializer + from agentspan.agents.tool import agent_tool + + retriever = ocg_agent( + name="ocg_us", + model="openai/gpt-4o-mini", + url="https://us.ocg.example.com", + credential="OCG_US_KEY", + ) + main = Agent( + name="main", + model="openai/gpt-4o", + instructions="Delegate retrieval.", + tools=[agent_tool(retriever)], + ) + + serialized = AgentConfigSerializer().serialize(main) + + at = serialized["tools"][0] + assert at["toolType"] == "agent_tool" + child = at["config"]["agentConfig"] + assert child["name"] == "ocg_us" + ocg_query = next(t for t in child["tools"] if t["name"] == "ocg_query") + assert ocg_query["toolType"] == "ocg_query" + assert ocg_query["config"]["url"] == "https://us.ocg.example.com" + assert ocg_query["config"]["credential"] == "OCG_US_KEY" + assert ocg_query["config"]["credentials"] == ["OCG_US_KEY"] diff --git a/server/conductor-agentspan-server/src/main/resources/application.properties b/server/conductor-agentspan-server/src/main/resources/application.properties index 810917ca5..9e346f229 100644 --- a/server/conductor-agentspan-server/src/main/resources/application.properties +++ b/server/conductor-agentspan-server/src/main/resources/application.properties @@ -168,15 +168,11 @@ agentspan.credentials.resolve.rate-limit=120 # ============================================================================= # OCG (Open Context Graph) Configuration # ============================================================================= -# Set OCG_URL to enable the OCG sub-agent. When set: -# - 7 OCG_* system tasks are registered -# - The _ocg_agent workflow is registered at startup -# - Every top-level agent is silently given an "ocg_agent" agent_tool, -# so the main agent's LLM can delegate to OCG when it needs context. -agentspan.ocg.url=${OCG_URL:} -agentspan.ocg.api-key=${OCG_API_KEY:} -# Required when OCG is enabled. Boot fails fast if OCG_URL is set but OCG_MODEL is not. -agentspan.ocg.model=${OCG_MODEL:} +# OCG agents and tools are declared in user code via the AgentSpan SDK +# (ocg_agent() / ocg_tools()), each binding its own OCG instance URL and +# credential-store reference. There is no server-side OCG instance +# configuration — this switch only registers the 7 OCG_* system tasks. +agentspan.ocg.enabled=${OCG_ENABLED:true} # Per-call response cap defaults to 8192 in OcgProperties. Uncomment to override. # agentspan.ocg.response-cap-chars=8192 diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/AgentCompilerTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/AgentCompilerTest.java index 7e7d3dc19..925631dbf 100644 --- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/AgentCompilerTest.java +++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/AgentCompilerTest.java @@ -1765,4 +1765,30 @@ void testCtxInjectMessageHasNoLiteralSeparator() { .as("LLM_CHAT_COMPLETE task name must be lowercase TaskDef alias") .isEqualTo("llm_chat_complete"); } + + @Test + void testCompileNeverInjectsUndeclaredTools() { + // Inverse of the deleted auto-expose merger tests: an agent's compiled + // workflow references exactly its declared tools. Server capabilities + // like OCG are opted into from the SDK, never appended at compile time. + ToolConfig tool = ToolConfig.builder() + .name("search") + .description("Search the web") + .inputSchema(Map.of("type", "object")) + .toolType("worker") + .build(); + AgentConfig config = AgentConfig.builder() + .name("plain_agent") + .model("openai/gpt-4o") + .instructions("You are helpful.") + .tools(List.of(tool)) + .build(); + + WorkflowDef wf = compiler.compile(config); + + assertThat(config.getTools()) + .as("compile must not mutate the declared tool list") + .hasSize(1); + assertThat(wf.toString()).doesNotContain("ocg_agent").doesNotContain("_ocg_agent"); + } } diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java deleted file mode 100644 index 39e2e3fc5..000000000 --- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/AutoExposedToolsMergeTest.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.compiler; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.jupiter.api.Test; - -import dev.agentspan.runtime.model.AgentConfig; -import dev.agentspan.runtime.model.ToolConfig; -import dev.agentspan.runtime.registry.RegisteredAgent; -import dev.agentspan.runtime.registry.RegisteredAgent.ExposeAsTool; - -/** - * Unit tests for {@link AutoExposedToolsMerger}. - * - *

    Pins the generic auto-expose mechanism that lets any server-side - * sub-agent become LLM-visible to every top-level agent just by declaring - * a {@link RegisteredAgent} bean with a non-null {@code autoExpose()}. - * These tests are intentionally not OCG-specific — OCG is one consumer; - * the contract here is the one every future consumer relies on.

    - */ -class AutoExposedToolsMergeTest { - - @Test - void appendsRegisteredAgentAsAgentTool() { - AutoExposedToolsMerger merger = new AutoExposedToolsMerger( - List.of(registeredAgent("_helper_agent", "helper_agent", "Use this when you need help."))); - - AgentConfig config = AgentConfig.builder() - .name("user_agent") - .tools(new ArrayList<>(List.of(workerTool("search")))) - .build(); - - merger.merge(config); - - assertThat(config.getTools()).hasSize(2); - ToolConfig injected = config.getTools().get(1); - assertThat(injected.getName()).isEqualTo("helper_agent"); - assertThat(injected.getToolType()).isEqualTo("agent_tool"); - // workflowName must match the WorkflowDef the registrar persists so - // SUB_WORKFLOW dispatch at runtime resolves to the right workflow — - // drift here would silently route to a missing workflow. - assertThat(injected.getConfig()).containsEntry("workflowName", "_helper_agent"); - assertThat(injected.getDescription()).isEqualTo("Use this when you need help."); - } - - @Test - void skipsAgentsThatDoNotRequestAutoExpose() { - // autoExpose() == null means "register the workflow but keep it - // invisible to user agents" (private helper, internal pipeline). - RegisteredAgent privateAgent = new RegisteredAgent() { - @Override - public AgentConfig agentConfig() { - return AgentConfig.builder().name("_private_agent").build(); - } - }; - AutoExposedToolsMerger merger = new AutoExposedToolsMerger(List.of(privateAgent)); - - AgentConfig config = AgentConfig.builder() - .name("user_agent") - .tools(new ArrayList<>(List.of(workerTool("search")))) - .build(); - - merger.merge(config); - - assertThat(config.getTools()).hasSize(1); - assertThat(config.getTools().get(0).getName()).isEqualTo("search"); - } - - @Test - void doesNotInjectIntoTheAutoExposedWorkflowItself() { - // Self-recursion guard: if the compile target IS the registered - // agent, the merger must skip it. Without this, the OCG agent's own - // compile would gain itself as a tool — broken tool list + infinite - // delegation. - AutoExposedToolsMerger merger = - new AutoExposedToolsMerger(List.of(registeredAgent("_helper_agent", "helper_agent", "irrelevant"))); - - AgentConfig config = AgentConfig.builder() - .name("_helper_agent") - .tools(new ArrayList<>(List.of(workerTool("internal_tool")))) - .build(); - - merger.merge(config); - - assertThat(config.getTools()).hasSize(1); - assertThat(config.getTools().get(0).getName()).isEqualTo("internal_tool"); - } - - @Test - void doesNotDuplicateWhenAToolWithThatNameAlreadyExists() { - // The caller's explicit declaration wins. Two entries with the - // same name would confuse the LLM's tool spec list and cause both - // dispatches to resolve to the same workflow. - AutoExposedToolsMerger merger = new AutoExposedToolsMerger( - List.of(registeredAgent("_helper_agent", "helper_agent", "auto description"))); - - ToolConfig existing = ToolConfig.builder() - .name("helper_agent") - .toolType("agent_tool") - .description("user-provided description") - .build(); - AgentConfig config = AgentConfig.builder() - .name("user_agent") - .tools(new ArrayList<>(List.of(existing))) - .build(); - - merger.merge(config); - - assertThat(config.getTools()).hasSize(1); - assertThat(config.getTools().get(0).getDescription()).isEqualTo("user-provided description"); - } - - @Test - void noOpWhenNoRegisteredAgentsArePresent() { - // The no-arg AgentCompiler constructor (used throughout the test - // suite) wires a disabled merger — compiles must work untouched. - AgentCompiler compiler = new AgentCompiler(); - - AgentConfig config = AgentConfig.builder() - .name("user_agent") - .model("openai/gpt-4o-mini") - .tools(new ArrayList<>(List.of(workerTool("search")))) - .build(); - - compiler.compile(config); - - assertThat(config.getTools()).hasSize(1); - } - - @Test - void appendsMultipleAgentsInBeanOrder() { - AutoExposedToolsMerger merger = new AutoExposedToolsMerger(List.of( - registeredAgent("_a_agent", "alpha_agent", "first"), - registeredAgent("_b_agent", "beta_agent", "second"))); - - AgentConfig config = AgentConfig.builder() - .name("user_agent") - .tools(new ArrayList<>()) - .build(); - - merger.merge(config); - - // Both registered agents surface as agent_tools on the same config. - // This is the path that lets future server-side capabilities - // accumulate without any per-feature wiring. - assertThat(config.getTools()).hasSize(2); - assertThat(config.getTools().get(0).getName()).isEqualTo("alpha_agent"); - assertThat(config.getTools().get(1).getName()).isEqualTo("beta_agent"); - } - - @Test - void compileEntryRunsTheMerge() { - // The public ``compile()`` is the single place user agents pick up - // auto-exposed tools — pin that the wiring from AgentCompiler into - // the merger actually fires. - AgentCompiler compiler = new AgentCompiler( - new AutoExposedToolsMerger(List.of(registeredAgent("_helper_agent", "helper_agent", "x")))); - - AgentConfig config = AgentConfig.builder() - .name("user_agent") - .model("openai/gpt-4o-mini") - .tools(new ArrayList<>()) - .build(); - - compiler.compile(config); - - assertThat(config.getTools()).extracting(ToolConfig::getName).contains("helper_agent"); - } - - @Test - void mergeRunsOnceAtTopLevelOnlyAndSkipsInternalRecursion() { - // Pinning the contract that the public ``compile()`` is the only entry - // that runs the auto-expose merge. Internal recursion (compileSubAgent, - // graph-structure subgraph compile, MultiAgentCompiler swarm) must go - // through the non-merging entry so nested specialist sub-agents don't - // silently pick up unrelated server-side tools. - AgentCompiler compiler = new AgentCompiler(new AutoExposedToolsMerger( - List.of(registeredAgent("_helper_agent", "helper_agent", "Use when stuck.")))); - - AgentConfig inner = AgentConfig.builder() - .name("inner_specialist") - .model("openai/gpt-4o-mini") - .build(); - - // compileSubAgent is the entry that nested compilation goes through. - // It must NOT mutate ``inner.tools`` with the auto-exposed entry. - compiler.compileSubAgent(inner, "inner_ref", "${workflow.input.prompt}", "${workflow.input.media}", null); - - boolean innerHasAutoExposed = - inner.getTools() != null && inner.getTools().stream().anyMatch(t -> "helper_agent".equals(t.getName())); - assertThat(innerHasAutoExposed) - .as("nested sub-agent must NOT have the auto-exposed tool merged into its tool list") - .isFalse(); - } - - @Test - void agentConfigReadOnceAtConstructionNotPerMerge() { - // Entries are fixed when the merger is constructed from the bean - // list — per-merge re-reads would be wasted work (registered agents - // don't change at runtime) and would re-trigger any validation in - // the agent's config factory on every user compile. - AtomicInteger reads = new AtomicInteger(); - RegisteredAgent counting = new RegisteredAgent() { - @Override - public AgentConfig agentConfig() { - reads.incrementAndGet(); - return AgentConfig.builder().name("_helper_agent").build(); - } - - @Override - public ExposeAsTool autoExpose() { - return new ExposeAsTool("helper_agent", "x"); - } - }; - AutoExposedToolsMerger merger = new AutoExposedToolsMerger(List.of(counting)); - - merger.merge(AgentConfig.builder().name("a").tools(new ArrayList<>()).build()); - merger.merge(AgentConfig.builder().name("b").tools(new ArrayList<>()).build()); - - assertThat(reads).hasValue(1); - } - - @Test - void blankToolNameFailsFastAtConstruction() { - // A blank LLM-facing tool name is a programming error in the - // RegisteredAgent bean — surfacing it at boot beats silently - // registering an unusable tool. - RegisteredAgent broken = new RegisteredAgent() { - @Override - public AgentConfig agentConfig() { - return AgentConfig.builder().name("_broken_agent").build(); - } - - @Override - public ExposeAsTool autoExpose() { - return new ExposeAsTool(" ", "description"); - } - }; - - assertThatThrownBy(() -> new AutoExposedToolsMerger(List.of(broken))) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("blank tool name"); - } - - // ───────────────────────────────────────────────────────────────────── - // Helpers - // ───────────────────────────────────────────────────────────────────── - - private static RegisteredAgent registeredAgent(String workflowName, String toolName, String description) { - return new RegisteredAgent() { - @Override - public AgentConfig agentConfig() { - return AgentConfig.builder().name(workflowName).build(); - } - - @Override - public ExposeAsTool autoExpose() { - return new ExposeAsTool(toolName, description); - } - }; - } - - private static ToolConfig workerTool(String name) { - return ToolConfig.builder().name(name).toolType("worker").build(); - } -} diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java deleted file mode 100644 index 5402b3817..000000000 --- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/RegisteredAgentBootstrapTest.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.compiler; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; - -import java.util.ArrayList; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import com.netflix.conductor.common.metadata.workflow.WorkflowDef; -import com.netflix.conductor.dao.MetadataDAO; - -import dev.agentspan.runtime.model.AgentConfig; -import dev.agentspan.runtime.model.ToolConfig; -import dev.agentspan.runtime.registry.RegisteredAgent; -import dev.agentspan.runtime.registry.RegisteredAgent.ExposeAsTool; -import dev.agentspan.runtime.registry.RegisteredAgentRegistrar; - -/** - * Bootstrap-ordering test for the {@link RegisteredAgentRegistrar} - * + auto-expose merger interaction. - * - *

    The merger reads its entries straight from the {@link RegisteredAgent} - * bean list at construction, so a user compile sees every registered agent - * regardless of whether the registrar's {@code @PostConstruct} DAO writes - * have happened yet. (The previous DAO-scan design could snapshot an empty - * cache during bootstrap and silently hide every registered agent for the - * bean's lifetime — this test pins that the trap is structurally gone.)

    - */ -class RegisteredAgentBootstrapTest { - - @Test - void userCompileSeesRegisteredAgentsRegardlessOfRegistrarOrdering() { - MetadataDAO dao = mock(MetadataDAO.class); - List daoState = new ArrayList<>(); - doAnswer(inv -> { - daoState.add(inv.getArgument(0)); - return null; - }) - .when(dao) - .updateWorkflowDef(any(WorkflowDef.class)); - - RegisteredAgent helper = new RegisteredAgent() { - @Override - public AgentConfig agentConfig() { - return AgentConfig.builder() - .name("_helper_agent") - .description("Test helper") - .model("openai/gpt-4o-mini") - .tools(new ArrayList<>()) - .build(); - } - - @Override - public ExposeAsTool autoExpose() { - return new ExposeAsTool("helper_tool", "Call when stuck."); - } - }; - - // Production wiring: the merger is built from the same bean list the - // registrar iterates. - AgentCompiler compiler = new AgentCompiler(new AutoExposedToolsMerger(List.of(helper))); - - // A compile BEFORE the registrar has written anything to the DAO — - // the exact window where the old DAO-scan design froze an empty - // cache — must already see the registered agent. - AgentConfig earlyAgent = AgentConfig.builder() - .name("early_agent") - .model("openai/gpt-4o-mini") - .tools(new ArrayList<>()) - .build(); - compiler.compile(earlyAgent); - assertThat(earlyAgent.getTools()) - .extracting(ToolConfig::getName) - .as("compile before registrar bootstrap must already see the registered agent") - .contains("helper_tool"); - - // Bootstrap: the registrar persists the WorkflowDef so SUB_WORKFLOW - // dispatch can resolve '_helper_agent' by name at runtime. - new RegisteredAgentRegistrar(compiler, dao, List.of(helper)).registerAll(); - assertThat(daoState).hasSize(1); - assertThat(daoState.get(0).getName()).isEqualTo("_helper_agent"); - - // And a compile after bootstrap sees it too, of course. - AgentConfig lateAgent = AgentConfig.builder() - .name("late_agent") - .model("openai/gpt-4o-mini") - .tools(new ArrayList<>()) - .build(); - compiler.compile(lateAgent); - assertThat(lateAgent.getTools()).extracting(ToolConfig::getName).contains("helper_tool"); - } -} diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/ToolCompilerTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/ToolCompilerTest.java index a5021ad20..fac5b64a5 100644 --- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/ToolCompilerTest.java +++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/ToolCompilerTest.java @@ -334,4 +334,115 @@ void testBuildToolCallRoutingWithResult_backwardCompatible() { assertThat(router.getType()).isEqualTo("SWITCH"); assertThat(router.getDecisionCases()).containsKey("tool_call"); } + + // ── selfDescribing marker (OrkesLLM contract) ──────────────────────── + + @Test + void testCompileToolSpecs_everySpecIsSelfDescribing() { + // Every AgentSpan-compiled tool spec is complete (name + description + + // inputSchema inline), so every spec must carry the selfDescribing + // marker that tells OrkesLLM to skip integration resolution. + List tools = List.of( + ToolConfig.builder() + .name("search") + .description("Search") + .toolType("worker") + .build(), + ToolConfig.builder() + .name("fetch") + .description("Fetch") + .toolType("http") + .config(Map.of("url", "https://example.com")) + .build(), + ToolConfig.builder() + .name("ocg_query") + .description("Query OCG") + .toolType("ocg_query") + .build(), + ToolConfig.builder() + .name("helper") + .description("Sub-agent") + .toolType("agent_tool") + .config(Map.of("workflowName", "helper_wf")) + .build()); + + List> specs = new ToolCompiler().compileToolSpecs(tools); + + assertThat(specs).hasSize(4); + for (Map spec : specs) { + assertThat(spec.get("selfDescribing")) + .as("spec '%s' must be marked selfDescribing", spec.get("name")) + .isEqualTo(Boolean.TRUE); + // The marker must ALSO ride configParams: conductor-ai's ToolSpec + // has no selfDescribing field, so the top-level key is dropped at + // deserialization — configParams is a Map that + // survives, letting OrkesLLM read it without a conductor-oss + // release. + assertThat(spec.get("configParams")) + .as("spec '%s' must carry the marker in configParams", spec.get("name")) + .isInstanceOfSatisfying(Map.class, cp -> assertThat(cp.get("selfDescribing")) + .isEqualTo(Boolean.TRUE)); + } + } + + @Test + void testCompileToolSpecs_markerMergesIntoExistingConfigParams() { + // MCP tools already populate configParams (mcpServer/headers) — the + // marker must merge in, not clobber them. + ToolConfig mcp = ToolConfig.builder() + .name("github") + .description("GitHub MCP") + .toolType("mcp") + .config(Map.of("server_url", "http://localhost:3001/mcp")) + .build(); + + Map spec = + new ToolCompiler().compileToolSpecs(List.of(mcp)).get(0); + + @SuppressWarnings("unchecked") + Map cp = (Map) spec.get("configParams"); + assertThat(cp.get("mcpServer")).isEqualTo("http://localhost:3001/mcp"); + assertThat(cp.get("selfDescribing")).isEqualTo(Boolean.TRUE); + } + + // ── OCG per-instance config plumbing ───────────────────────────────── + + @Test + void testBuildEnrichTask_ocgInstanceConfig() { + // An OCG tool bound to a specific instance carries its url and a + // bearer-credential placeholder into the baked ocgCfg, and the script + // merges them into the dispatched task input as __ocg_url/__ocg_auth. + ToolConfig tool = ToolConfig.builder() + .name("ocg_query") + .description("Query the US graph") + .toolType("ocg_query") + .config(Map.of("url", "https://us.ocg.example.com", "credential", "OCG_US_KEY")) + .build(); + + Object[] result = new ToolCompiler().buildEnrichTask("agent", "agent_llm", List.of(tool), ""); + String script = (String) ((WorkflowTask) result[0]).getInputParameters().get("expression"); + + assertThat(script).contains("\"url\":\"https://us.ocg.example.com\""); + // Standalone mode: ${OCG_US_KEY} is escaped to #{OCG_US_KEY} so + // Conductor's parameter binding doesn't consume it. + assertThat(script).contains("\"auth\":\"Bearer #{OCG_US_KEY}\""); + assertThat(script).contains("__ocg_url"); + assertThat(script).contains("__ocg_auth"); + } + + @Test + void testBuildEnrichTask_ocgDefaultInstance() { + // No url/credential in config → the baked entry carries only the + // task type; the system task falls back to the server default. + ToolConfig tool = ToolConfig.builder() + .name("ocg_query") + .description("Query OCG") + .toolType("ocg_query") + .build(); + + Object[] result = new ToolCompiler().buildEnrichTask("agent", "agent_llm", List.of(tool), ""); + String script = (String) ((WorkflowTask) result[0]).getInputParameters().get("expression"); + + assertThat(script).contains("\"ocg_query\":{\"taskType\":\"OCG_QUERY\"}"); + } } diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefsTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefsTest.java new file mode 100644 index 000000000..1e1dfae1f --- /dev/null +++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefsTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +class OcgRegisteredTaskDefsTest { + + @Test + void taskDefNamesMatchDispatchedTaskNames() { + // The enrich script schedules each OCG tool call as a task named + // ``taskType.toLowerCase()`` (e.g. OCG_QUERY → "ocg_query"). Conductor + // resolves dynamic-fork tasks by NAME, so the registered TaskDefs must + // use exactly those names — anything else fails dispatch with + // "Cannot find task by name ocg_query in the task definitions". + List names = new OcgRegisteredTaskDefs() + .taskDefs().stream().map(def -> def.getName()).toList(); + + assertThat(names) + .containsExactlyInAnyOrder( + "ocg_query", + "ocg_get_entity", + "ocg_neighborhood", + "ocg_code_history", + "ocg_memory_set", + "ocg_memory_reinforce", + "ocg_memory_delete"); + } +} diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java new file mode 100644 index 000000000..d2cf6cd10 --- /dev/null +++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.netflix.conductor.model.TaskModel; +import com.netflix.conductor.model.WorkflowModel; + +import dev.agentspan.runtime.ocg.operation.OcgQueryOperation; + +/** + * Per-call OCG instance resolution: the tool-bound instance + * ({@code __ocg_url} / {@code __ocg_auth} task inputs) is the only + * instance — there is no server-side default. A task dispatched without + * {@code __ocg_url} fails fast. + */ +class OcgRequestTaskTest { + + private HttpClient httpClient; + private HttpResponse response; + + @BeforeEach + @SuppressWarnings("unchecked") + void setUp() throws Exception { + httpClient = mock(HttpClient.class); + response = mock(HttpResponse.class); + when(response.statusCode()).thenReturn(200); + when(response.body()).thenReturn("{\"citations\":[]}"); + when(httpClient.send(any(), any(HttpResponse.BodyHandler.class))).thenReturn(response); + } + + private static OcgProperties props() { + return new OcgProperties(); + } + + private static TaskModel taskWithInput(Map input) { + TaskModel task = new TaskModel(); + task.setInputData(new HashMap<>(input)); + return task; + } + + private HttpRequest sentRequest() throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(HttpRequest.class); + verify(httpClient).send(captor.capture(), any()); + return captor.getValue(); + } + + @Test + void perToolUrlIsUsed() throws Exception { + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(), httpClient, null); + TaskModel model = taskWithInput(Map.of("query", "hello", "__ocg_url", "https://ca.ocg.example.com")); + + task.start(new WorkflowModel(), model, null); + + assertThat(model.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); + assertThat(sentRequest().uri().toString()).startsWith("https://ca.ocg.example.com/api/v1/"); + } + + @Test + void failsFastWithoutBoundInstance() { + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(), httpClient, null); + TaskModel model = taskWithInput(Map.of("query", "hello")); + + task.start(new WorkflowModel(), model, null); + + assertThat(model.getStatus()).isEqualTo(TaskModel.Status.FAILED); + assertThat(model.getReasonForIncompletion()).contains("no OCG instance").contains("url="); + verifyNoInteractions(httpClient); + } + + @Test + void preResolvedAuthHeaderIsSentVerbatim() throws Exception { + // Embedded mode: the host already substituted ${workflow.secrets.NAME}, + // so __ocg_auth arrives fully resolved. + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(), httpClient, null); + TaskModel model = taskWithInput(Map.of( + "query", "hello", + "__ocg_url", "https://us.ocg.example.com", + "__ocg_auth", "Bearer resolved-us-secret")); + + task.start(new WorkflowModel(), model, null); + + assertThat(model.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); + assertThat(sentRequest().headers().firstValue("Authorization")).hasValue("Bearer resolved-us-secret"); + } + + @Test + void placeholderAuthIsResolvedThroughCredentialResolver() throws Exception { + OcgCredentialResolver resolver = (value, ctx) -> value.replace("#{OCG_US_KEY}", "us-secret"); + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(), httpClient, resolver); + TaskModel model = taskWithInput(Map.of( + "query", "hello", + "__ocg_url", "https://us.ocg.example.com", + "__ocg_auth", "Bearer #{OCG_US_KEY}", + "__agentspan_ctx__", Map.of("execution_token", "tok"))); + + task.start(new WorkflowModel(), model, null); + + assertThat(model.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); + assertThat(sentRequest().headers().firstValue("Authorization")).hasValue("Bearer us-secret"); + } + + @Test + void unresolvablePlaceholderFailsTheTask() { + // Resolver returns null (unknown credential / invalid token): the task + // must fail rather than send the placeholder as a bearer token. + OcgCredentialResolver resolver = (value, ctx) -> null; + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(), httpClient, resolver); + TaskModel model = taskWithInput(Map.of( + "query", "hello", + "__ocg_url", "https://us.ocg.example.com", + "__ocg_auth", "Bearer #{OCG_US_KEY}")); + + task.start(new WorkflowModel(), model, null); + + assertThat(model.getStatus()).isEqualTo(TaskModel.Status.FAILED); + assertThat(model.getReasonForIncompletion()).contains("credential"); + verifyNoInteractions(httpClient); + } + + @Test + void noAuthHeaderWhenNoPerToolCredential() throws Exception { + // No credential bound → unauthenticated call; there is no server-side + // default key to silently attach. + OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(), httpClient, null); + TaskModel model = taskWithInput(Map.of("query", "hello", "__ocg_url", "https://us.ocg.example.com")); + + task.start(new WorkflowModel(), model, null); + + assertThat(model.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); + assertThat(sentRequest().headers().firstValue("Authorization")).isEmpty(); + } + + @Test + void reservedInputsAreNotForwardedToTheOcgApi() throws Exception { + // OcgMemorySetOperation posts the whole input map as the request body — + // the instance-binding keys must never leak into it. + OcgRequestTask task = new OcgRequestTask( + new dev.agentspan.runtime.ocg.operation.OcgMemorySetOperation(), props(), httpClient, null); + TaskModel model = taskWithInput(Map.of( + "key", + "k", + "agent", + "a", + "user", + "u", + "string_value", + "v", + "description", + "d", + "__ocg_url", + "https://us.ocg.example.com", + "__ocg_auth", + "Bearer resolved")); + + task.start(new WorkflowModel(), model, null); + + assertThat(model.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); + HttpRequest sent = sentRequest(); + String body = sent.bodyPublisher() + .map(p -> { + var collector = java.net.http.HttpResponse.BodySubscribers.ofString( + java.nio.charset.StandardCharsets.UTF_8); + p.subscribe(new java.util.concurrent.Flow.Subscriber<>() { + public void onSubscribe(java.util.concurrent.Flow.Subscription s) { + collector.onSubscribe(s); + s.request(Long.MAX_VALUE); + } + + public void onNext(java.nio.ByteBuffer item) { + collector.onNext(java.util.List.of(item)); + } + + public void onError(Throwable t) { + collector.onError(t); + } + + public void onComplete() { + collector.onComplete(); + } + }); + return collector.getBody().toCompletableFuture().join(); + }) + .orElse(""); + assertThat(body).doesNotContain("__ocg_url").doesNotContain("__ocg_auth"); + assertThat(body).contains("\"key\":\"k\""); + } +} diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgToolValidatorTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgToolValidatorTest.java new file mode 100644 index 000000000..77960f9fd --- /dev/null +++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgToolValidatorTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import dev.agentspan.runtime.model.AgentConfig; +import dev.agentspan.runtime.model.ToolConfig; + +class OcgToolValidatorTest { + + private static OcgProperties enabled() { + return new OcgProperties(); + } + + private static ToolConfig ocgTool(Map config) { + return ToolConfig.builder() + .name("ocg_query") + .toolType("ocg_query") + .description("Query OCG") + .config(config) + .build(); + } + + private static AgentConfig agentWith(ToolConfig... tools) { + return AgentConfig.builder() + .name("main") + .model("openai/gpt-4o") + .tools(List.of(tools)) + .build(); + } + + @Test + void ocgToolWithPerToolUrlIsValid() { + AgentConfig config = agentWith(ocgTool(Map.of("url", "https://us.ocg.example.com"))); + + assertThat(OcgToolValidator.validate(config, enabled())).isEmpty(); + } + + @Test + void ocgToolWithoutUrlIsRejected() { + // No server-side default instance exists — every OCG tool must bind + // its own url. + AgentConfig config = agentWith(ocgTool(null)); + + Optional error = OcgToolValidator.validate(config, enabled()); + + assertThat(error).isPresent(); + assertThat(error.get()).contains("ocg_query").contains("url="); + } + + @Test + void ocgToolIsRejectedWhenFeatureDisabled() { + AgentConfig config = agentWith(ocgTool(Map.of("url", "https://us.ocg.example.com"))); + + Optional error = OcgToolValidator.validate(config, null); + + assertThat(error).isPresent(); + assertThat(error.get()).contains("agentspan.ocg.enabled"); + } + + @Test + void nonOcgToolsAreIgnored() { + AgentConfig config = agentWith(ToolConfig.builder() + .name("fetch") + .toolType("http") + .config(Map.of("url", "https://example.com")) + .build()); + + assertThat(OcgToolValidator.validate(config, null)).isEmpty(); + } + + @Test + void instancelessOcgToolInsideInlineAgentToolChildIsRejected() { + // The SDK serializes agent_tool children as raw maps under + // config.agentConfig — the validator must walk that shape too. + Map childAgent = Map.of( + "name", + "retriever", + "tools", + List.of(Map.of( + "name", "ocg_query", + "toolType", "ocg_query"))); + ToolConfig agentTool = ToolConfig.builder() + .name("retriever") + .toolType("agent_tool") + .config(Map.of("agentConfig", childAgent)) + .build(); + + Optional error = OcgToolValidator.validate(agentWith(agentTool), enabled()); + + assertThat(error).isPresent(); + assertThat(error.get()).contains("ocg_query"); + } + + @Test + void instancelessOcgToolInSubAgentIsRejected() { + AgentConfig sub = agentWith(ocgTool(null)); + AgentConfig main = AgentConfig.builder() + .name("main") + .model("openai/gpt-4o") + .agents(List.of(sub)) + .build(); + + assertThat(OcgToolValidator.validate(main, enabled())).isPresent(); + } +} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java index 444c00d9d..81f7bad5d 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java @@ -10,7 +10,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @@ -45,32 +44,6 @@ public class AgentCompiler { private int contextMaxSizeBytes = 32768; private int contextMaxValueSizeBytes = 4096; - /** - * Owns the per-config merge of auto-exposed server-registered agents. - * Pulled out so {@code AgentCompiler} stays focused on workflow - * compilation; the merger sources its entries from the - * {@code RegisteredAgent} bean list at construction. - */ - private final AutoExposedToolsMerger autoExposedMerger; - - /** - * Default no-arg constructor for tests that don't need the auto-expose - * merge — it becomes a no-op. - */ - public AgentCompiler() { - this(AutoExposedToolsMerger.disabled()); - } - - /** - * Spring-injected constructor. The merger is itself a {@code @Component} - * built from the {@code RegisteredAgent} bean list, so this is the path - * the runtime uses. - */ - @Autowired - public AgentCompiler(AutoExposedToolsMerger autoExposedMerger) { - this.autoExposedMerger = autoExposedMerger; - } - /** * Sanitizes an agent name for use as a Conductor task reference name. * @@ -116,40 +89,12 @@ String getText() { } /** - * Public entry point: compile a top-level {@link AgentConfig} into a - * {@link WorkflowDef}. - * - *

    Before strategy dispatch, every auto-exposed - * {@code RegisteredAgent} is silently appended to {@code config.tools} - * as an {@code agent_tool}. That's how the OCG sub-agent (and any - * future server-side sub-agent) becomes LLM-visible without - * per-feature injection code.

    - * - *

    Internal recursion (nested sub-agents, graph-structure sub-compiles, - * MultiAgentCompiler swarm) must go through {@link #compileWithoutAutoExpose} - * instead so a specialist sub-agent isn't polluted with an unrelated - * retrieval tool.

    + * Public entry point: compile an {@link AgentConfig} into a + * {@link WorkflowDef}. An agent's compiled tool list is exactly its + * declared tool list — server-side capabilities (e.g. OCG) are opted + * into explicitly from the SDK, never injected here. */ public WorkflowDef compile(AgentConfig config) { - autoExposedMerger.merge(config); - return compileWithoutAutoExpose(config); - } - - /** - * Compile entry that performs strategy dispatch and post-processing - * without running the auto-expose merge. - * - *

    Two callers:

    - *
      - *
    • Internal recursion ({@link #compileSubAgent}, graph subgraph - * compile, {@code MultiAgentCompiler}) — nested specialist agents - * must not inherit unrelated server-side tools.
    • - *
    • {@link dev.agentspan.runtime.registry.RegisteredAgentRegistrar} - * at {@code @PostConstruct} time — registered server agents - * shouldn't have other registered agents auto-exposed to them.
    • - *
    - */ - public WorkflowDef compileWithoutAutoExpose(AgentConfig config) { WorkflowDef wf; // Passthrough check MUST be first — passthrough configs have null model. @@ -1079,10 +1024,7 @@ WorkflowTask compileSubAgent( task.getSubWorkflowParam().setName(sub.getName()); task.setInputParameters(inputs); } else { - // Compile inline. ``compileWithoutAutoExpose`` (not ``compile``) so - // the auto-expose merge stays a top-level-only concern — nested - // sub-agents must not inherit unrelated server-registered tools. - WorkflowDef subWf = compileWithoutAutoExpose(sub); + WorkflowDef subWf = compile(sub); task.setType("SUB_WORKFLOW"); task.setName(sub.getName()); task.setSubWorkflowParam(new SubWorkflowParams()); @@ -1883,10 +1825,8 @@ private SubgraphNodeResult buildSubgraphNodeTasks( List defaultTasks = new ArrayList<>(); if (subAgent != null) { - // Compile the subgraph into a WorkflowDef. ``compileWithoutAutoExpose`` - // because this is internal recursion — only the top-level compile - // entry runs the auto-expose merge. - WorkflowDef subWf = compileWithoutAutoExpose(subAgent); + // Compile the subgraph into a WorkflowDef. + WorkflowDef subWf = compile(subAgent); String subRef = allocRef(usedRefs, "_sg_sub_" + nodeName); WorkflowTask subTask = new WorkflowTask(); diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java deleted file mode 100644 index d5aa10dbd..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AutoExposedToolsMerger.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.compiler; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import dev.agentspan.runtime.model.AgentConfig; -import dev.agentspan.runtime.model.ToolConfig; -import dev.agentspan.runtime.registry.RegisteredAgent; -import dev.agentspan.runtime.registry.RegisteredAgent.ExposeAsTool; - -/** - * Owns the "auto-expose registered sub-agents as tools" contract for - * {@link AgentCompiler}. - * - *

    The contract: any {@link RegisteredAgent} bean whose - * {@link RegisteredAgent#autoExpose()} is non-null is silently appended to - * every top-level agent's tool list as an {@code agent_tool} at compile - * time. The OCG sub-agent (and any future server-registered sub-agent) - * becomes LLM-visible via this single mechanism — no per-feature injection - * class required.

    - * - *

    The entries are read straight from the Spring-managed bean list at - * construction time. The beans are also what {@code RegisteredAgentRegistrar} - * persists to the metadata store, so both sides share one source of truth - * and no DAO read-back is needed: the merger is complete the moment it is - * constructed, regardless of when the registrar's {@code @PostConstruct} - * writes happen.

    - */ -@Component -public class AutoExposedToolsMerger { - - private static final Logger log = LoggerFactory.getLogger(AutoExposedToolsMerger.class); - - /** - * Pairing of source workflow name + pre-built {@code agent_tool} - * {@link ToolConfig}, fixed at construction. The workflow name rides - * alongside the tool so the per-compile self-recursion guard works - * without re-reading the {@link AgentConfig}. - */ - private record AutoExposedEntry(String workflowName, ToolConfig tool) {} - - private final List entries; - - @Autowired - public AutoExposedToolsMerger(@Autowired(required = false) List registeredAgents) { - this.entries = buildEntries(registeredAgents != null ? registeredAgents : List.of()); - } - - /** A merger with no registered agents — always a no-op. For tests / direct construction. */ - public static AutoExposedToolsMerger disabled() { - return new AutoExposedToolsMerger(null); - } - - /** - * Append every auto-exposed registered agent to {@code config.tools} - * as an {@code agent_tool}. - * - *

    Mutates {@code config} in place. Skips when:

    - *
      - *
    • No {@link RegisteredAgent} bean requested auto-expose
    • - *
    • The agent being compiled IS the auto-exposed one - * — no self-recursion
    • - *
    • A tool with that name is already declared on the config - * — caller's explicit declaration wins
    • - *
    - */ - public void merge(AgentConfig config) { - if (config == null || entries.isEmpty()) return; - - Set takenNames = collectToolNames(config); - List toAppend = new ArrayList<>(); - for (AutoExposedEntry entry : entries) { - if (entry.workflowName().equals(config.getName())) continue; // self-recursion guard - if (!takenNames.add(entry.tool().getName())) continue; // caller's declaration wins - toAppend.add(entry.tool()); - log.info( - "Auto-exposed workflow '{}' as agent_tool '{}' on '{}'", - entry.workflowName(), - entry.tool().getName(), - config.getName()); - } - if (!toAppend.isEmpty()) { - appendTools(config, toAppend); - } - } - - private static List buildEntries(List registeredAgents) { - List built = new ArrayList<>(); - for (RegisteredAgent agent : registeredAgents) { - ExposeAsTool expose = agent.autoExpose(); - if (expose == null) continue; - if (expose.toolName() == null || expose.toolName().isBlank()) { - throw new IllegalStateException("RegisteredAgent " - + agent.getClass().getName() + " requested auto-expose with a blank tool name"); - } - String workflowName = agent.agentConfig().getName(); - built.add(new AutoExposedEntry(workflowName, buildAgentTool(workflowName, expose))); - } - return List.copyOf(built); - } - - private static Set collectToolNames(AgentConfig config) { - if (config.getTools() == null) return new HashSet<>(); - Set names = new HashSet<>(); - for (ToolConfig t : config.getTools()) { - if (t.getName() != null) names.add(t.getName()); - } - return names; - } - - private static ToolConfig buildAgentTool(String workflowName, ExposeAsTool expose) { - // Same schema OpenAINormalizer builds for user-declared agent tools — every - // agentspan tool is self-describing on the wire, so any host executor - // (standalone or embedded) can hand it to the LLM without type knowledge. - Map inputSchema = new LinkedHashMap<>(); - inputSchema.put("type", "object"); - inputSchema.put( - "properties", - Map.of( - "request", - Map.of( - "type", - "string", - "description", - "The request or question to send to this agent"))); - inputSchema.put("required", List.of("request")); - inputSchema.put("additionalProperties", false); - - return ToolConfig.builder() - .name(expose.toolName()) - .toolType("agent_tool") - .description(expose.toolDescription() != null ? expose.toolDescription() : "") - .inputSchema(inputSchema) - .config(Map.of("workflowName", workflowName)) - .build(); - } - - private static void appendTools(AgentConfig config, List toAppend) { - List merged = new ArrayList<>(config.getTools() != null ? config.getTools() : List.of()); - merged.addAll(toAppend); - config.setTools(merged); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java index 8bc14c16a..4ebf6ffaa 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java @@ -1437,9 +1437,7 @@ private WorkflowDef compileSwarmAgentWorkflowWithSubAgents(AgentConfig agent, Li String checkTransferRef = agent.getName() + "_check_transfer"; // 1. Compile the agent normally to preserve its multi-agent strategy. - // ``compileWithoutAutoExpose`` because this is internal recursion; - // the auto-expose merge only runs at the top-level public entry. - WorkflowDef innerWf = agentCompiler.compileWithoutAutoExpose(agent); + WorkflowDef innerWf = agentCompiler.compile(agent); // Inner agent as SUB_WORKFLOW WorkflowTask innerTask = new WorkflowTask(); diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java index f57be5744..201a8a422 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java @@ -110,7 +110,7 @@ private static Map escapeHeadersInConfig(Map cfg * same as RAG/media: the tool name keys into ``ocgConfig`` at runtime, and * the enrich script sets ``t.type`` to the configured task type. */ - static final Set OCG_TOOL_TYPES = Set.of( + public static final Set OCG_TOOL_TYPES = Set.of( "ocg_query", "ocg_get_entity", "ocg_neighborhood", @@ -165,6 +165,17 @@ public List> compileToolSpecs(List tools) { spec.put("name", tool.getName()); spec.put("type", conductorType); spec.put("description", tool.getDescription()); + // Every AgentSpan spec is complete as compiled (name + description + // + inputSchema inline). The marker tells spec consumers — notably + // orkes-conductor's OrkesLLM — to hand it to the LLM as-is and + // never resolve/replace it by name against integration stores. + // Kept top-level for a future first-class ToolSpec field, and + // duplicated into configParams at the end of this loop — that + // copy is the one that survives today: conductor-ai's ToolSpec + // has no selfDescribing field, so this top-level key is dropped + // at deserialization, while configParams (Map) + // rides through intact. + spec.put("selfDescribing", true); if (tool.getInputSchema() != null) { spec.put("inputSchema", tool.getInputSchema()); @@ -200,6 +211,18 @@ public List> compileToolSpecs(List tools) { spec.put("configParams", configParams); } + // Merge (never clobber — the MCP/API blocks above populate it + // first) the selfDescribing marker into configParams; see the + // top-level marker comment for why this copy is the one that + // survives deserialization. + @SuppressWarnings("unchecked") + Map markerParams = (Map) spec.get("configParams"); + if (markerParams == null) { + markerParams = new LinkedHashMap<>(); + spec.put("configParams", markerParams); + } + markerParams.put("selfDescribing", true); + specs.add(spec); } @@ -395,11 +418,22 @@ public Object[] buildEnrichTask(String agentName, String llmRef, List ocgEntry = new LinkedHashMap<>(); ocgEntry.put("taskType", TYPE_MAP.getOrDefault(toolType, toolType.toUpperCase())); + Object ocgUrl = cfg.get("url"); + if (ocgUrl != null && !ocgUrl.toString().isBlank()) { + ocgEntry.put("url", ocgUrl.toString()); + } + Object ocgCredential = cfg.get("credential"); + if (ocgCredential != null && !ocgCredential.toString().isBlank()) { + // Same escaping path as HTTP/MCP headers: the secret name + // becomes a placeholder resolved at execution time, never + // a value baked into the workflow definition. + ocgEntry.put("auth", rewriteCredentialPlaceholders("Bearer ${" + ocgCredential + "}")); + } ocgConfig.put(tool.getName(), ocgEntry); } } diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java deleted file mode 100644 index d831c0149..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgAgentFactory.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang3.StringUtils; - -import dev.agentspan.runtime.model.AgentConfig; -import dev.agentspan.runtime.model.ToolConfig; - -/** - * Builds the {@link AgentConfig} for the server-registered OCG sub-agent. - * - *

    The agent itself is a vanilla LLM-driven agent: it has instructions - * (the OCG retrieval prompt) and seven {@code ocg_*} tools that dispatch to - * the OCG_* system tasks registered by {@link OcgRequestTaskConfig}.

    - */ -public final class OcgAgentFactory { - - /** Stable workflow name for the registered OCG sub-agent. */ - public static final String AGENT_NAME = "_ocg_agent"; - - /** - * Tool name as the main agent's LLM sees it. Distinct from {@link #AGENT_NAME} - * because workflows registered server-side use the underscore-prefix - * convention while LLM-visible tool names don't. - */ - public static final String TOOL_NAME = "ocg_agent"; - - /** - * Description shown to the main agent's LLM in its tool spec list. Kept - * concrete enough that the model can decide when to delegate - * without leaking implementation details into the user-facing prompt. - */ - public static final String TOOL_DESCRIPTION = - "Delegate to the OCG (Open Context Graph) retrieval agent when you need " - + "context from the knowledge graph — message search, entity lookup, " - + "code history, or stored memories. Provide a focused natural-language " - + "query (under ~15 content words). Returns a synthesized answer with " - + "supporting citations."; - - /** - * Conductor expression the system prompt uses as its date anchor. - * Resolved per execution against the {@code _ocg_agent} sub-workflow's - * input — the agent_tool dispatch script injects {@code __today__} with - * the current UTC date on every call. Anchoring at execution time (not - * boot time) is what keeps "recent" / relative-date queries correct on - * a long-running server; a date baked in at registration would drift - * until the prompt claimed last month was "today". - */ - static final String TODAY_EXPRESSION = "${workflow.input.__today__}"; - - /** - * System prompt for the OCG sub-agent. {@link #TODAY_EXPRESSION} is - * substituted by Conductor when the LLM task is scheduled, so any - * "recent" / relative-date query gets bounded against the real current - * date instead of whatever year the model felt like inventing. - */ - static final String OCG_SYSTEM_PROMPT = "Today's date is " + TODAY_EXPRESSION + " (UTC). When a user asks for\n" - + "\"recent\" / \"last week\" / any relative range, anchor on this date.\n" - + "Never invent a date range — if no range is implied by the user, omit\n" - + "start_time/end_time from the request.\n" - + "If the range extends to the present (\"recent\", \"current state\",\n" - + "\"catch me up\", \"last N days\"), set start_time and OMIT end_time —\n" - + "the results then run through now. Only set end_time when the user\n" - + "asks about a window that closed in the past. Never end a range at a\n" - + "month boundary before today; that silently drops the newest data.\n\n" - + "You are querying an OCG (Observability Context Graph). It is a RETRIEVAL\n" - + "engine over a knowledge graph of entities (messages, channels, people)\n" - + "linked by claims and relationships. It is NOT an aggregation engine.\n\n" - + "It can answer:\n" - + " - \"Find messages in channel X about Y\"\n" - + " - \"Show TIMED_OUT errors for cluster \"\n" - + " - \"What entities mention 'health check failure'?\"\n" - + " - \"Recent messages in #cloud_saas_health_check_alerts\"\n\n" - + "It CANNOT directly answer (you must do it yourself in two steps):\n" - + " - \"How many of X are there?\" / \"Which X is most frequent?\"\n" - + " - \"Group these by Y\" / \"Top N by count\"\n" - + " - Statistical or comparative questions\n\n" - + "For aggregation questions, use a TWO-STEP pattern:\n" - + " 1. RETRIEVE: ask OCG for the raw set of relevant entities.\n" - + " - Use specific terms (cluster names, error codes, channel names).\n" - + " - Use start_time (and end_time only for windows closed in the\n" - + " past) to bound the range.\n" - + " - Set max_results high (e.g. 500) so you get the full set, not a\n" - + " top-N sample.\n" - + " - Avoid hedging words (\"frequently\", \"across\", \"occurrences\") —\n" - + " OCG ranks by keyword presence, and these are noise tokens.\n" - + " 2. AGGREGATE: count, group, rank yourself from the citation list.\n\n" - + "Query length: keep it under ~15 content words. Long prompts dilute the\n" - + "BM25 keyword set; OCG's parser is extracting things like \"happen\",\n" - + "\"identify\", \"top one\" which are not real signal.\n\n" - + "Bad: \"Across all clusters, what alert/notification/error type appears\n" - + " most frequently? Group similar alerts and tell me which one has\n" - + " the highest count and how many clusters it affected.\"\n\n" - + "Good (step 1): {\n" - + " \"query\": \"TIMED_OUT health check failure cluster\",\n" - + " \"max_results\": 500,\n" - + " \"start_time\": \"T00:00:00Z\"\n" - + "}\n" - + "(end_time omitted — the range runs through now.)\n" - + "Then parse the returned citations, extract cluster names from titles,\n" - + "build the frequency table in your reasoning."; - - private OcgAgentFactory() {} - - public static AgentConfig build(OcgProperties props) { - if (StringUtils.isBlank(props.getModel())) { - throw new IllegalArgumentException( - "OCG is enabled (agentspan.ocg.url is set) but agentspan.ocg.model is blank. " - + "Set OCG_MODEL (or -Dagentspan.ocg.model=…) to the LLM the OCG sub-agent " - + "should use, e.g. OCG_MODEL=openai/gpt-4o-mini."); - } - return AgentConfig.builder() - .name(AGENT_NAME) - .description("Retrieval sub-agent over the Open Context Graph (OCG).") - .model(props.getModel()) - .instructions(OCG_SYSTEM_PROMPT) - .tools(buildTools()) - .maxTurns(10) - .build(); - } - - static List buildTools() { - return List.of( - queryTool(), - getEntityTool(), - neighborhoodTool(), - codeHistoryTool(), - memorySetTool(), - memoryReinforceTool(), - memoryDeleteTool()); - } - - private static ToolConfig queryTool() { - Map properties = new LinkedHashMap<>(); - properties.put("query", schema("string", "Natural-language retrieval query.")); - properties.put("max_results", schema("integer", "Max citations to return.", 10)); - properties.put( - "traversal_level", schema("integer", "0 = citations only, 1 = neighborhood, 2-3 = multi-hop.", 1)); - properties.put("start_time", schema("string", "ISO-8601 lower bound (inclusive). Optional.")); - properties.put("end_time", schema("string", "ISO-8601 upper bound (exclusive). Optional.")); - return ToolConfig.builder() - .name("ocg_query") - .toolType("ocg_query") - .description("Query the Open Context Graph for structured retrieval. " - + "Returns citations (source_item_id, title, container_id, snippet) " - + "and traversal_results when traversal_level > 0.") - .inputSchema(objectSchema(properties, List.of("query"))) - .build(); - } - - private static ToolConfig getEntityTool() { - Map properties = new LinkedHashMap<>(); - properties.put("entity_id", schema("string", "Canonical entity id from an ocg_query result row.")); - return ToolConfig.builder() - .name("ocg_get_entity") - .toolType("ocg_get_entity") - .description("Fetch one entity by its canonical id.") - .inputSchema(objectSchema(properties, List.of("entity_id"))) - .build(); - } - - private static ToolConfig neighborhoodTool() { - Map properties = new LinkedHashMap<>(); - properties.put("entity_id", schema("string", "Entity at the center of the neighborhood.")); - properties.put("depth", schema("integer", "Hop depth (use depth=1 on first call).", 2)); - properties.put("limit", schema("integer", "Cap on neighbors returned (use <= 10 on first call).", 50)); - return ToolConfig.builder() - .name("ocg_neighborhood") - .toolType("ocg_neighborhood") - .description("Get an entity plus its graph neighbors out to `depth` hops. " - + "Use limit <= 10, depth=1 on the first call — well-connected " - + "entities can have many edges and large responses will be truncated.") - .inputSchema(objectSchema(properties, List.of("entity_id"))) - .build(); - } - - private static ToolConfig codeHistoryTool() { - Map properties = new LinkedHashMap<>(); - properties.put("repo_id", schema("string", "Ingested repository id.")); - properties.put("path", schema("string", "Path within the repo.")); - properties.put("limit", schema("integer", "Max commits to return.", 20)); - return ToolConfig.builder() - .name("ocg_code_history") - .toolType("ocg_code_history") - .description("Last N commits that touched a file in an ingested repo.") - .inputSchema(objectSchema(properties, List.of("repo_id", "path"))) - .build(); - } - - private static ToolConfig memorySetTool() { - Map properties = new LinkedHashMap<>(); - properties.put("key", schema("string", "Memory key.")); - properties.put("agent", schema("string", "Agent owner (e.g. \"agent:\").")); - properties.put("user", schema("string", "User owner (e.g. \"user:\").")); - properties.put("string_value", schema("string", "Stored value.")); - properties.put("description", schema("string", "Human-readable description.")); - properties.put( - "scope", - schema( - "string", - "Memory scope. One of MEMORY_SCOPE_SESSION, MEMORY_SCOPE_AGENT, MEMORY_SCOPE_USER, MEMORY_SCOPE_SHARED, MEMORY_SCOPE_GLOBAL.", - "MEMORY_SCOPE_USER")); - properties.put("confidence", schema("number", "Inferred confidence in [0,1]. Cap at 0.7.", 0.7)); - properties.put("source_ref", schema("string", "Free-form source reference (e.g. message id).")); - properties.put("evidence_ids", arraySchema("string", "Supporting evidence entity ids.")); - properties.put("tags", arraySchema("string", "Tags.")); - properties.put("expires_at", schema("string", "ISO-8601 expiry. Optional — default 180 days.")); - properties.put("idempotency_key", schema("string", "Idempotency key. Optional.")); - return ToolConfig.builder() - .name("ocg_memory_set") - .toolType("ocg_memory_set") - .description("Create or overwrite a memory in OCG. Cap inferred confidence at 0.7; " - + "never write PII or secrets.") - .inputSchema(objectSchema(properties, List.of("key", "agent", "user", "string_value", "description"))) - .build(); - } - - private static ToolConfig memoryReinforceTool() { - Map properties = new LinkedHashMap<>(); - properties.put("key", schema("string", "Memory key.")); - properties.put("agent", schema("string", "Agent owner.")); - properties.put("user", schema("string", "User owner.")); - properties.put( - "confidence_boost", - schema("number", "Boost to add (must be <= 0.05 to prevent compounding drift).", 0.05)); - properties.put("source_ref", schema("string", "Free-form source reference.")); - return ToolConfig.builder() - .name("ocg_memory_reinforce") - .toolType("ocg_memory_reinforce") - .description("Reinforce an existing memory on independent re-observation. " - + "confidence_boost must be <= 0.05.") - .inputSchema(objectSchema(properties, List.of("key", "agent", "user"))) - .build(); - } - - private static ToolConfig memoryDeleteTool() { - Map properties = new LinkedHashMap<>(); - properties.put("key", schema("string", "Memory key.")); - properties.put("agent", schema("string", "Agent owner.")); - properties.put("user", schema("string", "User owner.")); - return ToolConfig.builder() - .name("ocg_memory_delete") - .toolType("ocg_memory_delete") - .description("Delete a memory by key. Prefer ocg_memory_set with a corrected value " - + "over deletion (preserves history).") - .inputSchema(objectSchema(properties, List.of("key", "agent", "user"))) - .build(); - } - - // ───────────────────────────────────────────────────────────────────── - // JSON-schema helpers - // ───────────────────────────────────────────────────────────────────── - - private static Map objectSchema(Map properties, List required) { - Map schema = new LinkedHashMap<>(); - schema.put("type", "object"); - schema.put("properties", properties); - if (!required.isEmpty()) { - schema.put("required", required); - } - return schema; - } - - private static Map schema(String type, String description) { - Map s = new LinkedHashMap<>(); - s.put("type", type); - s.put("description", description); - return s; - } - - private static Map schema(String type, String description, Object defaultValue) { - Map s = schema(type, description); - s.put("default", defaultValue); - return s; - } - - private static Map arraySchema(String itemType, String description) { - Map s = new LinkedHashMap<>(); - s.put("type", "array"); - s.put("description", description); - s.put("items", Map.of("type", itemType)); - return s; - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgCredentialResolver.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgCredentialResolver.java new file mode 100644 index 000000000..96488ea41 --- /dev/null +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgCredentialResolver.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +/** + * Resolves {@code #{NAME}} credential placeholders in a per-tool OCG auth + * header value (standalone mode). In embedded mode the host substitutes + * {@code ${workflow.secrets.NAME}} before the task ever starts, so values + * arrive with no placeholders and this resolver is never consulted. + * + *

    Mirrors the contract of {@code CredentialAwareHttpTask}: resolution is + * scoped to the calling user via the execution token in + * {@code __agentspan_ctx__}, and resolved values exist only in memory — + * they are never written back to the task model.

    + */ +@FunctionalInterface +public interface OcgCredentialResolver { + + /** + * Resolve every {@code #{NAME}} placeholder in {@code value}. + * + * @param value the auth header value, e.g. {@code "Bearer #{OCG_US_KEY}"} + * @param agentspanCtx the {@code __agentspan_ctx__} task input (map with an + * {@code execution_token} entry, or the raw token string) + * @return the fully resolved value, or {@code null} when resolution is not + * possible (missing/invalid token, unknown credential name) + */ + String resolve(String value, Object agentspanCtx); +} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java index b2c7829a8..90a318106 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java @@ -10,36 +10,24 @@ import lombok.Data; /** - * Configuration for the optional OCG (Open Context Graph) sub-agent. + * Configuration for the OCG (Open Context Graph) execution layer. * - *

    When {@code agentspan.ocg.url} is set, the server registers a specialized - * retrieval sub-agent at startup, exposes seven {@code OCG_*} system tasks - * that make HTTP calls to OCG with response capping + field projection, and - * auto-injects {@code _ocg_agent} as an {@code agent_tool} into every - * top-level agent so the main agent's LLM can decide to delegate to OCG - * when it needs context.

    + *

    OCG agents and tools are declared in user code via the SDK + * ({@code ocg_agent()} / {@code ocg_tools()}), and every OCG tool binds its + * own instance (url + credential-store reference) — there is no server-side + * OCG instance configuration. These properties only control whether the + * execution layer exists and how responses are shaped.

    */ @Data @ConfigurationProperties(prefix = "agentspan.ocg") public class OcgProperties { - /** Base URL of the OCG service. Empty / null disables the entire OCG feature. */ - private String url; - - /** - * Bearer token sent on every OCG HTTP request as - * {@code Authorization: Bearer }. Empty / null means no auth - * header — useful for local dev against an unauthenticated OCG instance. - */ - private String apiKey; - /** - * Model the OCG sub-agent uses for its own LLM turns. Required when OCG - * is enabled — no silent default, because the right model here depends - * on cost, latency, and the OCG corpus the operator is querying. Boot - * fails fast in {@link OcgAgentFactory#build} when this is blank. + * Whether the OCG execution layer (the {@code OCG_*} system tasks) is + * available. Disabling rejects agent starts that declare OCG tools. + * (Lombok generates {@code isEnabled()} for this field.) */ - private String model; + private boolean enabled = true; /** * Per-response truncation cap (post-projection, JSON-serialized) for the @@ -47,12 +35,4 @@ public class OcgProperties { * {@code _enforce_response_cap}. */ private int responseCapChars = 8192; - - public boolean isEnabled() { - return url != null && !url.isBlank(); - } - - public boolean hasApiKey() { - return apiKey != null && !apiKey.isBlank(); - } } diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java deleted file mode 100644 index c879d0b2e..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredAgent.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.stereotype.Component; - -import dev.agentspan.runtime.model.AgentConfig; -import dev.agentspan.runtime.registry.RegisteredAgent; - -import lombok.RequiredArgsConstructor; - -/** - * OCG's contribution to the {@link RegisteredAgent} registry: the - * server-registered sub-agent that the main agent's LLM can delegate to - * for context retrieval. - * - *

    Picked up automatically by {@code RegisteredAgentRegistrar} on boot, - * compiled to a {@code WorkflowDef}, stamped with the auto-expose - * metadata marker (via {@link #autoExpose()}), and written to the - * metadata store. From the next user-agent compile onward, - * {@code ocg_agent} appears in every LLM's tool list.

    - * - *

    The {@code @ConditionalOnExpression} uses {@code .length() > 0} - * rather than the more obvious {@code @ConditionalOnProperty} because - * the latter treats an empty string as "present and not false" and - * would instantiate this bean for unset {@code OCG_URL}, breaking the - * tests that rely on OCG being off by default.

    - */ -@Component -@ConditionalOnExpression("'${agentspan.ocg.url:}'.length() > 0") -@RequiredArgsConstructor -public class OcgRegisteredAgent implements RegisteredAgent { - - private final OcgProperties properties; - - @Override - public AgentConfig agentConfig() { - return OcgAgentFactory.build(properties); - } - - @Override - public ExposeAsTool autoExpose() { - return new ExposeAsTool(OcgAgentFactory.TOOL_NAME, OcgAgentFactory.TOOL_DESCRIPTION); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java index e552eb40c..daaccf325 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java @@ -7,7 +7,7 @@ import java.util.List; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import com.netflix.conductor.common.metadata.tasks.TaskDef; @@ -31,21 +31,28 @@ * {@code OcgRequestTask} and the parent LLM loop owns retry decisions.

    */ @Component -@ConditionalOnExpression("'${agentspan.ocg.url:}'.length() > 0") +@ConditionalOnProperty(prefix = "agentspan.ocg", name = "enabled", havingValue = "true", matchIfMissing = true) public class OcgRegisteredTaskDefs implements RegisteredTaskDefs { private static final int OCG_TASK_TIMEOUT_SECONDS = 60; private static final int OCG_TASK_RETRY_COUNT = 0; private static final String OCG_TASK_OWNER_EMAIL = "ocg@agentspan.dev"; + /** + * TaskDef names must equal {@code taskType.toLowerCase()} — that is the + * name the tool-dispatch script assigns to each scheduled OCG task, and + * Conductor resolves dynamic-fork tasks by name. The operation NAME + * constants ("query", "memory_set", …) are log/output labels, not task + * names — registering those leaves "ocg_query" unresolvable at dispatch. + */ private static final List TASK_NAMES = List.of( - OcgQueryOperation.NAME, - OcgGetEntityOperation.NAME, - OcgNeighborhoodOperation.NAME, - OcgCodeHistoryOperation.NAME, - OcgMemorySetOperation.NAME, - OcgMemoryReinforceOperation.NAME, - OcgMemoryDeleteOperation.NAME); + OcgQueryOperation.TASK_TYPE.toLowerCase(), + OcgGetEntityOperation.TASK_TYPE.toLowerCase(), + OcgNeighborhoodOperation.TASK_TYPE.toLowerCase(), + OcgCodeHistoryOperation.TASK_TYPE.toLowerCase(), + OcgMemorySetOperation.TASK_TYPE.toLowerCase(), + OcgMemoryReinforceOperation.TASK_TYPE.toLowerCase(), + OcgMemoryDeleteOperation.TASK_TYPE.toLowerCase()); @Override public List taskDefs() { diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java index db79a3c78..52100db2c 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java @@ -13,6 +13,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; +import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -25,21 +26,40 @@ import dev.agentspan.runtime.ocg.operation.OcgInputs; import dev.agentspan.runtime.ocg.operation.OcgOperation; +import dev.agentspan.runtime.ocg.operation.OcgTarget; /** * System task that proxies a single OCG (Open Context Graph) operation. * - *

    This class is the thin orchestrator: enabled-check → send → project → + *

    This class is the thin orchestrator: resolve target → send → project → * cap → COMPLETED/FAILED. The endpoint-specific work (URL, method, body, * field projection) lives in the strategy passed via {@link OcgOperation}. * One {@link OcgRequestTask} bean per operation is registered by * {@link OcgRequestTaskConfig}; the bean name is the operation's task * type, which Conductor's {@code SystemTaskRegistry} dispatches on.

    + * + *

    Instance resolution, per call: a tool-bound instance arrives as + * the reserved {@code __ocg_url} / {@code __ocg_auth} task inputs (compiled + * from the SDK's {@code url=} / {@code credential=}) — there is no + * server-side default instance. {@code __ocg_auth} + * may carry a {@code #{NAME}} placeholder in standalone mode — resolved + * in-memory via {@link OcgCredentialResolver}, never written back to the + * task model. The reserved inputs are stripped before the operation sees + * the input map so they cannot leak into request bodies.

    */ public class OcgRequestTask extends WorkflowSystemTask { private static final Logger log = LoggerFactory.getLogger(OcgRequestTask.class); + /** Reserved task-input key: per-tool OCG base URL. */ + public static final String INPUT_URL = "__ocg_url"; + + /** Reserved task-input key: per-tool Authorization header value. */ + public static final String INPUT_AUTH = "__ocg_auth"; + + private static final String INPUT_CTX = "__agentspan_ctx__"; + private static final Pattern PLACEHOLDER = Pattern.compile("#\\{[\\w.]+}"); + private static final String TRUNCATE_MARKER = "...[truncated]"; private static final int LOG_BODY_LIMIT = 256; private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10); @@ -47,28 +67,68 @@ public class OcgRequestTask extends WorkflowSystemTask { private final OcgOperation operation; private final OcgProperties properties; private final HttpClient httpClient; + private final OcgCredentialResolver credentialResolver; - public OcgRequestTask(OcgOperation operation, OcgProperties properties) { - this(operation, properties, defaultHttpClient()); + public OcgRequestTask(OcgOperation operation, OcgProperties properties, OcgCredentialResolver resolver) { + this(operation, properties, defaultHttpClient(), resolver); } /** Visible-for-testing constructor with an injectable {@link HttpClient}. */ OcgRequestTask(OcgOperation operation, OcgProperties properties, HttpClient httpClient) { + this(operation, properties, httpClient, null); + } + + /** + * Full constructor. {@code credentialResolver} may be null — per-tool + * {@code #{NAME}} auth placeholders then fail the task instead of + * leaking unresolved into the Authorization header. + */ + OcgRequestTask( + OcgOperation operation, + OcgProperties properties, + HttpClient httpClient, + OcgCredentialResolver credentialResolver) { super(Objects.requireNonNull(operation, "operation").taskType()); this.operation = operation; this.properties = Objects.requireNonNull(properties, "properties"); this.httpClient = Objects.requireNonNull(httpClient, "httpClient"); + this.credentialResolver = credentialResolver; log.debug("OcgRequestTask registered (taskType={}, operation={})", operation.taskType(), operation.name()); } @Override public void start(WorkflowModel workflow, TaskModel task, WorkflowExecutor executor) { - if (!properties.isEnabled()) { - fail(task, "OCG is not configured (agentspan.ocg.url is empty)"); + Map rawInput = task.getInputData() != null ? task.getInputData() : Map.of(); + + String url = stringInput(rawInput.get(INPUT_URL)); + if (url == null) { + fail( + task, + "OCG " + operation.name() + " has no OCG instance bound: set url= on " + + "ocg_agent()/ocg_tools() in the SDK"); return; } + + String auth = stringInput(rawInput.get(INPUT_AUTH)); + if (auth != null) { + if (PLACEHOLDER.matcher(auth).find()) { + String resolved = + credentialResolver != null ? credentialResolver.resolve(auth, rawInput.get(INPUT_CTX)) : null; + if (resolved == null || PLACEHOLDER.matcher(resolved).find()) { + fail( + task, + "OCG " + operation.name() + " credential could not be resolved — check that the " + + "credential name exists in the credential store and the execution " + + "token is valid"); + return; + } + auth = resolved; + } + } + + OcgTarget target = new OcgTarget(url, auth); try { - HttpResponse response = send(task); + HttpResponse response = send(rawInput, target); if (response.statusCode() < 200 || response.statusCode() >= 300) { fail( task, @@ -89,12 +149,25 @@ public void start(WorkflowModel workflow, TaskModel task, WorkflowExecutor execu } } - private HttpResponse send(TaskModel task) throws IOException, InterruptedException { - Map input = task.getInputData() != null ? task.getInputData() : Map.of(); - HttpRequest request = operation.build(properties, input); + private HttpResponse send(Map rawInput, OcgTarget target) + throws IOException, InterruptedException { + // Strip the instance-binding inputs so operations never see them — + // OcgMemorySetOperation forwards the whole map as the request body. + Map input = new LinkedHashMap<>(rawInput); + input.remove(INPUT_URL); + input.remove(INPUT_AUTH); + HttpRequest request = operation.build(target, input); return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); } + private static String stringInput(Object value) { + if (value == null) { + return null; + } + String s = value.toString(); + return s.isBlank() ? null : s; + } + private void complete(TaskModel task, String body) throws IOException { Object parsed = OcgInputs.parseJsonLenient(body); Object projected = operation.project(parsed); diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java index 59e6322e6..b5a2edde4 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java @@ -5,11 +5,14 @@ package dev.agentspan.runtime.ocg; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import dev.agentspan.runtime.credentials.CredentialResolutionService; +import dev.agentspan.runtime.credentials.ExecutionTokenService; import dev.agentspan.runtime.ocg.operation.OcgCodeHistoryOperation; import dev.agentspan.runtime.ocg.operation.OcgGetEntityOperation; import dev.agentspan.runtime.ocg.operation.OcgMemoryDeleteOperation; @@ -19,55 +22,72 @@ import dev.agentspan.runtime.ocg.operation.OcgQueryOperation; /** - * Registers one {@link OcgRequestTask} bean per OCG operation when - * {@code agentspan.ocg.url} is set. Bean names match the Conductor task - * type strings so {@code SystemTaskRegistry} looks them up by type at - * dispatch time. + * Registers one {@link OcgRequestTask} bean per OCG operation. Bean names + * match the Conductor task type strings so {@code SystemTaskRegistry} looks + * them up by type at dispatch time. * - *

    Each {@code @Bean} method pairs a fresh {@code OcgRequestTask} with - * a stateless operation strategy — the strategies own the per-endpoint - * URL/method/body/projection details; {@code OcgRequestTask} only knows - * how to send and shape the result.

    + *

    Gated on {@code agentspan.ocg.enabled} (default {@code true}) rather + * than on a global URL: OCG instances are bound per-tool from the SDK + * ({@code url=} + {@code credential=}), so the tasks must exist even when + * no server-wide default instance is configured. A task dispatched with + * neither a per-tool URL nor {@code agentspan.ocg.url} fails with a + * pointer to both knobs.

    */ @Configuration @EnableConfigurationProperties(OcgProperties.class) -// Empty agentspan.ocg.url means "feature off" — must use an expression -// because @ConditionalOnProperty matches empty strings. -@ConditionalOnExpression("'${agentspan.ocg.url:}'.length() > 0") +@ConditionalOnProperty(prefix = "agentspan.ocg", name = "enabled", havingValue = "true", matchIfMissing = true) public class OcgRequestTaskConfig { + /** + * Placeholder resolution for per-tool {@code #{NAME}} auth values + * (standalone mode). When the credential services are absent (embedded + * hosts resolve {@code ${workflow.secrets.NAME}} themselves before the + * task starts), unresolved placeholders fail the task instead. + */ + @Bean + public OcgCredentialResolver ocgCredentialResolver( + ObjectProvider tokenService, + ObjectProvider resolutionService) { + ExecutionTokenService tokens = tokenService.getIfAvailable(); + CredentialResolutionService credentials = resolutionService.getIfAvailable(); + if (tokens == null || credentials == null) { + return (value, ctx) -> null; + } + return new PlaceholderCredentialResolver(tokens, credentials); + } + @Bean(OcgQueryOperation.TASK_TYPE) - public OcgRequestTask ocgQueryTask(OcgProperties properties) { - return new OcgRequestTask(new OcgQueryOperation(), properties); + public OcgRequestTask ocgQueryTask(OcgProperties properties, OcgCredentialResolver resolver) { + return new OcgRequestTask(new OcgQueryOperation(), properties, resolver); } @Bean(OcgGetEntityOperation.TASK_TYPE) - public OcgRequestTask ocgGetEntityTask(OcgProperties properties) { - return new OcgRequestTask(new OcgGetEntityOperation(), properties); + public OcgRequestTask ocgGetEntityTask(OcgProperties properties, OcgCredentialResolver resolver) { + return new OcgRequestTask(new OcgGetEntityOperation(), properties, resolver); } @Bean(OcgNeighborhoodOperation.TASK_TYPE) - public OcgRequestTask ocgNeighborhoodTask(OcgProperties properties) { - return new OcgRequestTask(new OcgNeighborhoodOperation(), properties); + public OcgRequestTask ocgNeighborhoodTask(OcgProperties properties, OcgCredentialResolver resolver) { + return new OcgRequestTask(new OcgNeighborhoodOperation(), properties, resolver); } @Bean(OcgCodeHistoryOperation.TASK_TYPE) - public OcgRequestTask ocgCodeHistoryTask(OcgProperties properties) { - return new OcgRequestTask(new OcgCodeHistoryOperation(), properties); + public OcgRequestTask ocgCodeHistoryTask(OcgProperties properties, OcgCredentialResolver resolver) { + return new OcgRequestTask(new OcgCodeHistoryOperation(), properties, resolver); } @Bean(OcgMemorySetOperation.TASK_TYPE) - public OcgRequestTask ocgMemorySetTask(OcgProperties properties) { - return new OcgRequestTask(new OcgMemorySetOperation(), properties); + public OcgRequestTask ocgMemorySetTask(OcgProperties properties, OcgCredentialResolver resolver) { + return new OcgRequestTask(new OcgMemorySetOperation(), properties, resolver); } @Bean(OcgMemoryReinforceOperation.TASK_TYPE) - public OcgRequestTask ocgMemoryReinforceTask(OcgProperties properties) { - return new OcgRequestTask(new OcgMemoryReinforceOperation(), properties); + public OcgRequestTask ocgMemoryReinforceTask(OcgProperties properties, OcgCredentialResolver resolver) { + return new OcgRequestTask(new OcgMemoryReinforceOperation(), properties, resolver); } @Bean(OcgMemoryDeleteOperation.TASK_TYPE) - public OcgRequestTask ocgMemoryDeleteTask(OcgProperties properties) { - return new OcgRequestTask(new OcgMemoryDeleteOperation(), properties); + public OcgRequestTask ocgMemoryDeleteTask(OcgProperties properties, OcgCredentialResolver resolver) { + return new OcgRequestTask(new OcgMemoryDeleteOperation(), properties, resolver); } } diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgToolValidator.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgToolValidator.java new file mode 100644 index 000000000..57221d2ae --- /dev/null +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgToolValidator.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import dev.agentspan.runtime.compiler.ToolCompiler; +import dev.agentspan.runtime.model.AgentConfig; +import dev.agentspan.runtime.model.ToolConfig; + +/** + * Start-time fail-fast for OCG tools: an OCG tool without a bound + * instance ({@code url} in its config) would otherwise surface as a task + * failure mid-conversation. Checked + * recursively — sub-agents and inline {@code agent_tool} children (which + * arrive as raw maps from the SDK serializer) carry OCG tools too. + */ +public final class OcgToolValidator { + + private OcgToolValidator() {} + + /** + * @param config the agent about to start (recursed into) + * @param properties the server's OCG properties, or {@code null} when the + * OCG feature is disabled ({@code agentspan.ocg.enabled=false}) + * @return an error message when the start request must be rejected + */ + public static Optional validate(AgentConfig config, OcgProperties properties) { + if (config == null) { + return Optional.empty(); + } + boolean featureEnabled = properties != null; + + Optional error = validateTools(config.getTools(), featureEnabled); + if (error.isPresent()) { + return error; + } + if (config.getAgents() != null) { + for (AgentConfig sub : config.getAgents()) { + error = validate(sub, properties); + if (error.isPresent()) { + return error; + } + } + } + return Optional.empty(); + } + + private static Optional validateTools(List tools, boolean featureEnabled) { + if (tools == null) { + return Optional.empty(); + } + for (ToolConfig tool : tools) { + String toolType = tool.getToolType(); + Map cfg = tool.getConfig(); + + if (toolType != null && ToolCompiler.OCG_TOOL_TYPES.contains(toolType)) { + Optional error = checkOcgTool(tool.getName(), cfg, featureEnabled); + if (error.isPresent()) { + return error; + } + } else if ("agent_tool".equals(toolType) && cfg != null) { + Optional error = validateChild(cfg.get("agentConfig"), featureEnabled); + if (error.isPresent()) { + return error; + } + } + } + return Optional.empty(); + } + + /** + * Inline {@code agent_tool} children arrive either as typed + * {@link AgentConfig} (built server-side) or as the raw map the SDK + * serializer produced — both shapes are walked. + */ + @SuppressWarnings("unchecked") + private static Optional validateChild(Object child, boolean featureEnabled) { + if (child instanceof AgentConfig typed) { + return validate(typed, featureEnabled ? new OcgProperties() : null); + } + if (!(child instanceof Map childMap)) { + return Optional.empty(); + } + + Object tools = childMap.get("tools"); + if (tools instanceof List toolList) { + for (Object t : toolList) { + if (!(t instanceof Map toolMap)) { + continue; + } + String toolType = asString(toolMap.get("toolType")); + Map cfg = toolMap.get("config") instanceof Map m ? (Map) m : null; + if (toolType != null && ToolCompiler.OCG_TOOL_TYPES.contains(toolType)) { + Optional error = checkOcgTool(asString(toolMap.get("name")), cfg, featureEnabled); + if (error.isPresent()) { + return error; + } + } else if ("agent_tool".equals(toolType) && cfg != null) { + Optional error = validateChild(cfg.get("agentConfig"), featureEnabled); + if (error.isPresent()) { + return error; + } + } + } + } + Object agents = childMap.get("agents"); + if (agents instanceof List agentList) { + for (Object sub : agentList) { + Optional error = validateChild(sub, featureEnabled); + if (error.isPresent()) { + return error; + } + } + } + return Optional.empty(); + } + + private static Optional checkOcgTool(String name, Map cfg, boolean featureEnabled) { + String toolName = name != null ? name : "(unnamed)"; + if (!featureEnabled) { + return Optional.of("OCG tool '" + toolName + "' cannot run: OCG is disabled on this server " + + "(agentspan.ocg.enabled=false). Remove the OCG tools or enable OCG."); + } + String url = cfg != null ? asString(cfg.get("url")) : null; + if (url == null || url.isBlank()) { + return Optional.of("OCG tool '" + toolName + "' has no OCG instance bound: set url= on " + + "ocg_agent()/ocg_tools() in the SDK."); + } + return Optional.empty(); + } + + private static String asString(Object value) { + return value != null ? value.toString() : null; + } +} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/PlaceholderCredentialResolver.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/PlaceholderCredentialResolver.java new file mode 100644 index 000000000..d8ce3181d --- /dev/null +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/PlaceholderCredentialResolver.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.agentspan.runtime.credentials.CredentialResolutionService; +import dev.agentspan.runtime.credentials.ExecutionTokenService; + +/** + * Default {@link OcgCredentialResolver}: resolves {@code #{NAME}} placeholders + * through the credential store, scoped to the user identified by the execution + * token in {@code __agentspan_ctx__}. Same token/store contract as + * {@code CredentialAwareHttpTask} — OCG must not invent a second secret path. + */ +public class PlaceholderCredentialResolver implements OcgCredentialResolver { + + private static final Logger log = LoggerFactory.getLogger(PlaceholderCredentialResolver.class); + private static final Pattern PLACEHOLDER = Pattern.compile("#\\{([\\w.]+)}"); + + private final ExecutionTokenService tokenService; + private final CredentialResolutionService resolutionService; + + public PlaceholderCredentialResolver( + ExecutionTokenService tokenService, CredentialResolutionService resolutionService) { + this.tokenService = tokenService; + this.resolutionService = resolutionService; + } + + @Override + public String resolve(String value, Object agentspanCtx) { + if (value == null || !PLACEHOLDER.matcher(value).find()) { + return value; + } + String userId = extractUserId(agentspanCtx); + if (userId == null) { + return null; + } + Matcher m = PLACEHOLDER.matcher(value); + StringBuilder sb = new StringBuilder(); + while (m.find()) { + String credValue = resolutionService.resolve(userId, m.group(1)); + if (credValue == null) { + log.warn("OCG credential '{}' not found for user", m.group(1)); + return null; + } + m.appendReplacement(sb, Matcher.quoteReplacement(credValue)); + } + m.appendTail(sb); + return sb.toString(); + } + + private String extractUserId(Object ctx) { + String token = null; + if (ctx instanceof Map ctxMap) { + token = (String) ctxMap.get("execution_token"); + } else if (ctx instanceof String s) { + token = s; + } + if (token == null) { + return null; + } + try { + return tokenService.validate(token).userId(); + } catch (Exception e) { + log.warn("Failed to validate token for OCG credential resolution: {}", e.getMessage()); + return null; + } + } +} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java index bfdf12447..e8459835d 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java @@ -9,8 +9,6 @@ import java.net.http.HttpRequest; import java.util.Map; -import dev.agentspan.runtime.ocg.OcgProperties; - /** {@code GET /api/v1/code/history/{repo_id}?path=...&limit=N} — file commit history. */ public final class OcgCodeHistoryOperation implements OcgOperation { @@ -30,16 +28,16 @@ public String name() { } @Override - public HttpRequest build(OcgProperties properties, Map input) { + public HttpRequest build(OcgTarget target, Map input) { String repoId = OcgInputs.required(input, "repo_id"); String path = OcgInputs.required(input, "path"); - URI uri = OcgUri.forApi(properties) + URI uri = OcgUri.forApi(target) .pathSegment("code", "history", repoId) .queryParam("path", path) .queryParam("limit", OcgInputs.intOrDefault(input.get("limit"), DEFAULT_LIMIT)) .build() .toUri(); - return OcgRequest.get(properties, uri); + return OcgRequest.get(target, uri); } @Override diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java index fad99aed0..a3bba85d5 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java @@ -9,8 +9,6 @@ import java.net.http.HttpRequest; import java.util.Map; -import dev.agentspan.runtime.ocg.OcgProperties; - /** {@code GET /api/v1/entities/{entity_id}} — single entity lookup by id. */ public final class OcgGetEntityOperation implements OcgOperation { @@ -28,13 +26,11 @@ public String name() { } @Override - public HttpRequest build(OcgProperties properties, Map input) { + public HttpRequest build(OcgTarget target, Map input) { String entityId = OcgInputs.required(input, "entity_id"); - URI uri = OcgUri.forApi(properties) - .pathSegment("entities", entityId) - .build() - .toUri(); - return OcgRequest.get(properties, uri); + URI uri = + OcgUri.forApi(target).pathSegment("entities", entityId).build().toUri(); + return OcgRequest.get(target, uri); } @Override diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java index 29d3e8ace..e4175a505 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java @@ -11,8 +11,6 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.web.util.UriComponentsBuilder; -import dev.agentspan.runtime.ocg.OcgProperties; - /** {@code DELETE /api/v1/memories/{key}?agent=...&user=...} — remove a memory by key. */ public final class OcgMemoryDeleteOperation implements OcgOperation { @@ -30,12 +28,12 @@ public String name() { } @Override - public HttpRequest build(OcgProperties properties, Map input) { + public HttpRequest build(OcgTarget target, Map input) { String key = OcgInputs.required(input, "key"); - UriComponentsBuilder uri = OcgUri.forApi(properties).pathSegment("memories", key); + UriComponentsBuilder uri = OcgUri.forApi(target).pathSegment("memories", key); addQueryParamIfPresent(uri, "agent", input.get("agent")); addQueryParamIfPresent(uri, "user", input.get("user")); - return OcgRequest.delete(properties, uri.build().toUri()); + return OcgRequest.delete(target, uri.build().toUri()); } private static void addQueryParamIfPresent(UriComponentsBuilder uri, String name, Object value) { diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java index 0daf0c1ee..f52051453 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java @@ -10,8 +10,6 @@ import java.net.http.HttpRequest; import java.util.Map; -import dev.agentspan.runtime.ocg.OcgProperties; - /** {@code POST /api/v1/memories/{key}/reinforce} — confidence boost on re-observation. */ public final class OcgMemoryReinforceOperation implements OcgOperation { @@ -29,13 +27,13 @@ public String name() { } @Override - public HttpRequest build(OcgProperties properties, Map input) throws IOException { + public HttpRequest build(OcgTarget target, Map input) throws IOException { String key = OcgInputs.required(input, "key"); Map body = OcgInputs.pick(input, "agent", "user", "confidence_boost", "source_ref"); - URI uri = OcgUri.forApi(properties) + URI uri = OcgUri.forApi(target) .pathSegment("memories", key, "reinforce") .build() .toUri(); - return OcgRequest.postJson(properties, uri, body); + return OcgRequest.postJson(target, uri, body); } } diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java index 8e82a2447..f40a1b25d 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java @@ -11,8 +11,6 @@ import java.util.LinkedHashMap; import java.util.Map; -import dev.agentspan.runtime.ocg.OcgProperties; - /** * {@code POST /api/v1/memories} — create or overwrite a memory. * @@ -37,10 +35,10 @@ public String name() { } @Override - public HttpRequest build(OcgProperties properties, Map input) throws IOException { + public HttpRequest build(OcgTarget target, Map input) throws IOException { Map body = new LinkedHashMap<>(input); body.remove("__agentspan_ctx__"); - URI uri = OcgUri.forApi(properties).pathSegment("memories").build().toUri(); - return OcgRequest.postJson(properties, uri, body); + URI uri = OcgUri.forApi(target).pathSegment("memories").build().toUri(); + return OcgRequest.postJson(target, uri, body); } } diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java index 4f43b9bce..626a8dfaf 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java @@ -9,8 +9,6 @@ import java.net.http.HttpRequest; import java.util.Map; -import dev.agentspan.runtime.ocg.OcgProperties; - /** {@code GET /api/v1/graph/neighborhood/{entity_id}?depth=N&limit=M} — graph traversal. */ public final class OcgNeighborhoodOperation implements OcgOperation { @@ -31,15 +29,15 @@ public String name() { } @Override - public HttpRequest build(OcgProperties properties, Map input) { + public HttpRequest build(OcgTarget target, Map input) { String entityId = OcgInputs.required(input, "entity_id"); - URI uri = OcgUri.forApi(properties) + URI uri = OcgUri.forApi(target) .pathSegment("graph", "neighborhood", entityId) .queryParam("depth", OcgInputs.intOrDefault(input.get("depth"), DEFAULT_DEPTH)) .queryParam("limit", OcgInputs.intOrDefault(input.get("limit"), DEFAULT_LIMIT)) .build() .toUri(); - return OcgRequest.get(properties, uri); + return OcgRequest.get(target, uri); } @Override diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java index 61ad4a186..ad8984d64 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java @@ -9,8 +9,6 @@ import java.net.http.HttpRequest; import java.util.Map; -import dev.agentspan.runtime.ocg.OcgProperties; - /** * Strategy for a single OCG endpoint. One implementation per {@code OCG_*} * task type; each owns the URL/method/body for its endpoint plus the @@ -38,7 +36,7 @@ public interface OcgOperation { * implementations may throw — request building is purely an I/O-shape * concern and should not surface arbitrary checked exceptions.

    */ - HttpRequest build(OcgProperties properties, Map input) throws IOException; + HttpRequest build(OcgTarget target, Map input) throws IOException; /** * Project the parsed JSON response down to the fields the LLM needs. diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java index 862cd0ab5..2f41cca13 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java @@ -13,8 +13,6 @@ import java.util.List; import java.util.Map; -import dev.agentspan.runtime.ocg.OcgProperties; - /** {@code POST /api/v1/agent/query} — natural-language retrieval over the graph. */ public final class OcgQueryOperation implements OcgOperation { @@ -32,12 +30,11 @@ public String name() { } @Override - public HttpRequest build(OcgProperties properties, Map input) throws IOException { + public HttpRequest build(OcgTarget target, Map input) throws IOException { Map body = OcgInputs.pick(input, "query", "max_results", "traversal_level", "start_time", "end_time"); - URI uri = - OcgUri.forApi(properties).pathSegment("agent", "query").build().toUri(); - return OcgRequest.postJson(properties, uri, body); + URI uri = OcgUri.forApi(target).pathSegment("agent", "query").build().toUri(); + return OcgRequest.postJson(target, uri, body); } /** diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java index aa1e60a8b..b5f6afa15 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java @@ -12,8 +12,6 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; -import dev.agentspan.runtime.ocg.OcgProperties; - /** * HTTP request factory for OCG endpoints. Centralises the bearer-auth * header, default timeouts, and JSON body serialization so every @@ -32,18 +30,18 @@ private OcgRequest() {} * timeout, JSON accept, and the optional bearer token. Operations call * {@code .uri(...).METHOD(...).build()} on the returned builder. */ - public static HttpRequest.Builder base(OcgProperties properties) { + public static HttpRequest.Builder base(OcgTarget target) { HttpRequest.Builder b = HttpRequest.newBuilder().timeout(READ_TIMEOUT).header("Accept", "application/json"); - if (properties.hasApiKey()) { - b.header("Authorization", "Bearer " + properties.getApiKey()); + if (target.hasAuth()) { + b.header("Authorization", target.authHeader()); } return b; } /** POST with a JSON-serialized body. */ - public static HttpRequest postJson(OcgProperties properties, URI uri, Object body) throws IOException { + public static HttpRequest postJson(OcgTarget target, URI uri, Object body) throws IOException { String json = OcgInputs.writeJson(body); - return base(properties) + return base(target) .uri(uri) .header("Content-Type", "application/json") .POST(BodyPublishers.ofString(json, StandardCharsets.UTF_8)) @@ -51,12 +49,12 @@ public static HttpRequest postJson(OcgProperties properties, URI uri, Object bod } /** GET with no body. */ - public static HttpRequest get(OcgProperties properties, URI uri) { - return base(properties).uri(uri).GET().build(); + public static HttpRequest get(OcgTarget target, URI uri) { + return base(target).uri(uri).GET().build(); } /** DELETE with no body. */ - public static HttpRequest delete(OcgProperties properties, URI uri) { - return base(properties).uri(uri).DELETE().build(); + public static HttpRequest delete(OcgTarget target, URI uri) { + return base(target).uri(uri).DELETE().build(); } } diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgTarget.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgTarget.java new file mode 100644 index 000000000..e093ec7b3 --- /dev/null +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgTarget.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.ocg.operation; + +/** + * The OCG instance a single call targets: base URL plus the full + * {@code Authorization} header value (already resolved — no credential + * placeholders survive past {@code OcgRequestTask}). + * + *

    Resolved per call by {@code OcgRequestTask}: a tool-bound instance + * (the {@code __ocg_url} / {@code __ocg_auth} task inputs compiled from the + * SDK's {@code url=} / {@code credential=}) wins over the server-wide + * default in {@code OcgProperties}. Operations only ever see this record, + * so they cannot accidentally reach for the default config.

    + * + * @param baseUrl base URL of the OCG instance (no trailing slash required) + * @param authHeader full Authorization header value (e.g. {@code "Bearer …"}), + * or {@code null} / blank for unauthenticated instances + */ +public record OcgTarget(String baseUrl, String authHeader) { + + public boolean hasAuth() { + return authHeader != null && !authHeader.isBlank(); + } +} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java index 247ff634f..7db7942de 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java @@ -8,8 +8,6 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.web.util.UriComponentsBuilder; -import dev.agentspan.runtime.ocg.OcgProperties; - /** * URI builder for OCG endpoints. Every OCG path sits under {@code /api/v1} * on the configured base URL; this helper handles the trailing-slash @@ -32,8 +30,8 @@ private OcgUri() {} * {@code /api/v1}. Operations chain {@code .pathSegment(...)} * + {@code .queryParam(...)} on top. */ - public static UriComponentsBuilder forApi(OcgProperties properties) { - String base = StringUtils.removeEnd(StringUtils.defaultString(properties.getUrl()), "/"); + public static UriComponentsBuilder forApi(OcgTarget target) { + String base = StringUtils.removeEnd(StringUtils.defaultString(target.baseUrl()), "/"); return UriComponentsBuilder.fromUriString(base + API_PREFIX_V1); } } diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java deleted file mode 100644 index c63f8fd2f..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredAgent.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.registry; - -import dev.agentspan.runtime.model.AgentConfig; - -/** - * Marker for a Spring bean that contributes a server-registered agent. - * - *

    Any {@code @Bean} declared as {@link RegisteredAgent} is picked up by - * {@link RegisteredAgentRegistrar} on startup, compiled into a - * {@code WorkflowDef}, and persisted to Conductor's metadata store. Adding - * a new server-side sub-agent is therefore a one-bean change — no - * per-feature {@code @PostConstruct}, no manual {@code MetadataDAO} - * write, no duplication of the compile/persist ceremony.

    - * - *

    Implementations should be stateless or read configuration via Spring - * injection. {@link #agentConfig()} must be pure — it is invoked at - * startup by both the registrar (to compile and persist) and - * {@code AutoExposedToolsMerger} (to read the workflow name).

    - */ -public interface RegisteredAgent { - - /** - * The agent definition to compile and register. The returned - * {@link AgentConfig} owns name, model, instructions, tools — the - * registrar does not touch any of these fields. - */ - AgentConfig agentConfig(); - - /** - * When non-null, {@code AutoExposedToolsMerger} reads this spec - * directly from the bean and appends the agent as an - * {@code agent_tool} on every top-level user-agent compile. Return - * {@code null} to register the workflow without exposing it as a - * tool. - */ - default ExposeAsTool autoExpose() { - return null; - } - - /** - * The LLM-facing name and description used by the auto-expose path. - * The name is what users' agents will see in their tool spec list; - * the description is the LLM's only hint about when to - * delegate. - */ - record ExposeAsTool(String toolName, String toolDescription) {} -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java deleted file mode 100644 index 367f1a90f..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredAgentRegistrar.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.registry; - -import java.util.List; - -import jakarta.annotation.PostConstruct; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.DependsOn; -import org.springframework.stereotype.Component; - -import com.netflix.conductor.common.metadata.workflow.WorkflowDef; -import com.netflix.conductor.dao.MetadataDAO; - -import dev.agentspan.runtime.compiler.AgentCompiler; -import dev.agentspan.runtime.model.AgentConfig; -import dev.agentspan.runtime.registry.RegisteredAgent.ExposeAsTool; - -/** - * Generic registrar that drives every {@link RegisteredAgent} bean through - * the same compile → persist pipeline on server startup. - * - *

    Replaces feature-specific {@code @PostConstruct registerWorkflow()} - * methods that previously coupled OCG (and future sub-agents) to the - * mechanics of metadata-store writes. Adding a new server-side sub-agent - * now only requires declaring a {@code @Bean RegisteredAgent}.

    - * - *

    LLM visibility is not this class's job: {@code AutoExposedToolsMerger} - * reads {@link RegisteredAgent#autoExpose()} straight from the bean list, - * so the persisted {@link WorkflowDef} only needs to exist for - * SUB_WORKFLOW dispatch to resolve it by name at runtime.

    - */ -@Component -@DependsOn("registeredTaskDefsRegistrar") -public class RegisteredAgentRegistrar { - - private static final Logger log = LoggerFactory.getLogger(RegisteredAgentRegistrar.class); - - private final AgentCompiler agentCompiler; - private final MetadataDAO metadataDAO; - private final List registeredAgents; - - @Autowired - public RegisteredAgentRegistrar( - AgentCompiler agentCompiler, - MetadataDAO metadataDAO, - @Autowired(required = false) List registeredAgents) { - this.agentCompiler = agentCompiler; - this.metadataDAO = metadataDAO; - this.registeredAgents = registeredAgents != null ? registeredAgents : List.of(); - } - - @PostConstruct - public void registerAll() { - for (RegisteredAgent agent : registeredAgents) { - register(agent); - } - if (!registeredAgents.isEmpty()) { - log.info("Registered {} server-side agent(s)", registeredAgents.size()); - } - } - - private void register(RegisteredAgent agent) { - AgentConfig config = agent.agentConfig(); - // ``compileWithoutAutoExpose`` (not ``compile``) — registered agents - // shouldn't have other registered agents auto-injected into them as - // tools. - WorkflowDef def = agentCompiler.compileWithoutAutoExpose(config); - metadataDAO.updateWorkflowDef(def); - ExposeAsTool expose = agent.autoExpose(); - log.info( - "Registered agent: workflow='{}'{}", - def.getName(), - expose != null ? " autoExposeAs='" + expose.toolName() + "'" : ""); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java index 9cbe97d4f..6cb09d9e8 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java @@ -24,9 +24,8 @@ * Generic registrar that writes every {@link RegisteredTaskDefs}-contributed * {@link TaskDef} into Conductor's metadata store on startup. * - *

    {@link RegisteredAgentRegistrar} declares an explicit dependency on - * this bean via {@code @DependsOn} so task defs are written before any - * agent workflow compiles — agent configs frequently reference task + *

    Runs at {@code @PostConstruct} time so task defs are written before + * any agent workflow compiles — agent configs frequently reference task * names whose defs must already exist.

    */ @Component @@ -56,9 +55,7 @@ public void registerAll() { // updateTaskDef semantics (OSS DAO upserts; orkes' service throws NOT_FOUND // for unknown names), so check existence against the full list first. Set existing = - metadataService.getTaskDefs().stream() - .map(TaskDef::getName) - .collect(Collectors.toSet()); + metadataService.getTaskDefs().stream().map(TaskDef::getName).collect(Collectors.toSet()); List toCreate = new ArrayList<>(); int updated = 0; for (RegisteredTaskDefs supplier : suppliers) { diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java index 8e82e3440..b17d0f6ae 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java @@ -48,6 +48,8 @@ import dev.agentspan.runtime.credentials.ExecutionTokenService; import dev.agentspan.runtime.model.*; import dev.agentspan.runtime.normalizer.NormalizerRegistry; +import dev.agentspan.runtime.ocg.OcgProperties; +import dev.agentspan.runtime.ocg.OcgToolValidator; import dev.agentspan.runtime.util.ModelParser; import dev.agentspan.runtime.util.ProviderValidator; @@ -96,6 +98,14 @@ public class AgentService { @Autowired(required = false) private MetadataService metadataService; + /** + * OCG execution-layer config. Null when {@code agentspan.ocg.enabled=false} + * (the bean only exists while {@code OcgRequestTaskConfig} is active), which + * {@link OcgToolValidator} treats as "OCG tools are unavailable". + */ + @Autowired(required = false) + private OcgProperties ocgProperties; + /** Package-private constructor for testing with ExecutionTokenService */ AgentService( AgentCompiler agentCompiler, @@ -256,6 +266,12 @@ public StartResponse start(StartRequest request) { validateStartInput(request); AgentConfig config = resolveConfig(request); + // Fail fast on OCG tools with no instance to run against — otherwise + // this surfaces as a task failure mid-conversation. + OcgToolValidator.validate(config, ocgProperties).ifPresent(err -> { + throw new IllegalArgumentException(err); + }); + // Apply per-call timeout override from StartRequest if (request.getTimeoutSeconds() != null && request.getTimeoutSeconds() > 0) { config.setTimeoutSeconds(request.getTimeoutSeconds()); diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java index 997825f28..79a416338 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java @@ -644,12 +644,15 @@ public static String enrichToolsScript( + " t.inputParameters = merged;" + " } else if (ocgCfg[n]) {" // OCG tools dispatch to per-operation OCG_* system tasks. The - // OCG URL is resolved server-side from OcgProperties, so the - // script only needs to set the task type and forward the - // LLM-supplied arguments verbatim as inputParameters. + // LLM-supplied arguments are forwarded verbatim; a per-tool + // instance binding (url + auth placeholder) rides along as + // reserved __ocg_* inputs. Tools without one fall back to the + // server default (OcgProperties) inside OcgRequestTask. + " t.type = ocgCfg[n].taskType;" + " t.name = ocgCfg[n].taskType.toLowerCase();" + " t.inputParameters = tc.inputParameters || {};" + + " if (ocgCfg[n].url) { t.inputParameters.__ocg_url = ocgCfg[n].url; }" + + " if (ocgCfg[n].auth) { t.inputParameters.__ocg_auth = ocgCfg[n].auth; }" + " if ($.agentspanCtx) { t.inputParameters.__agentspan_ctx__ = $.agentspanCtx; }" + " } else if (humanCfg[n]) {" + " t.type = 'HUMAN';" @@ -1281,6 +1284,8 @@ public static String enrichToolsScriptDynamic( + " t.type = ocgCfg[n].taskType;" + " t.name = ocgCfg[n].taskType.toLowerCase();" + " t.inputParameters = tc.inputParameters || {};" + + " if (ocgCfg[n].url) { t.inputParameters.__ocg_url = ocgCfg[n].url; }" + + " if (ocgCfg[n].auth) { t.inputParameters.__ocg_auth = ocgCfg[n].auth; }" + " if ($.agentspanCtx) { t.inputParameters.__agentspan_ctx__ = $.agentspanCtx; }" + " } else if (humanCfg[n]) {" + " t.type = 'HUMAN';" From 6414f0d2b8cab6c1309cdcf1f6411ff76f5142b7 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Fri, 12 Jun 2026 13:22:35 -0700 Subject: [PATCH 29/61] OCG tools compile to plain HTTP tasks; delete the OCG execution layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OCG API returns LLM-friendly responses, which removed the last justification (response projection/capping) for OCG-specific server code. OCG is now pure SDK content executing on stock Conductor HTTP tasks. Server: - Delete the entire runtime/ocg package: OcgRequestTask + the seven operation strategies, OcgTarget/OcgUri/OcgRequest/OcgInputs, OcgToolValidator, OcgCredentialResolver/PlaceholderCredentialResolver, OcgProperties, OcgRegisteredTaskDefs, OcgRequestTaskConfig — plus the ocg_* TYPE_MAP entries, OCG_TOOL_TYPES, the ocgCfg enrich branch and plumbing (both script copies), the AgentService wiring, and every agentspan.ocg.* property. No OCG references remain server-side. - New generally-available http tool capability: configs may declare pathTemplate (e.g. /api/v1/entities/{entity_id}) and queryParams; the dispatch script fills them from the LLM's arguments (URL-encoded), appends present query params, and prunes consumed args from the body. http tools without these keys are byte-identical to before. Covered by GraalJS execution tests (EnrichToolsScriptTest) and compile-shape tests. SDK: - ocg_tools() emits tool_type="http" ToolDefs; the per-operation routing (method, pathTemplate, queryParams) lives in agentspan/agents/ocg.py — the single home of the whole OCG integration. Auth is the standard http-tool header placeholder (Authorization: Bearer ${CRED}), riding the same credential pipeline as every http_tool. - Public API unchanged: ocg_agent(model=, url=, credential=) — examples, smoke, and the two-stub e2e needed no changes. Validation: full server suite green; SDK 39/39; two-stub e2e 3/3 on the HttpTask path (US/Canada instance isolation on recorded traffic). For embedding hosts: agentspan.ocg.enabled / OCG_ENABLED are now dead config; stale ocg_* TaskDefs in the metadata store are harmless leftovers. Docs, release note, and the orkes handoff plan updated. Co-Authored-By: Claude Fable 5 --- .../2026-06-12-ocg-sdk-subagent-status.md | 17 ++ ...2026-06-12-toolspec-selfdescribing-plan.md | 25 +-- docs/ocg-agent-flow.md | 111 ++++------ docs/python-sdk/api-reference.md | 8 +- .../2026-06-12-ocg-sdk-subagent.md | 21 +- sdk/python/src/agentspan/agents/ocg.py | 70 ++++-- sdk/python/tests/unit/test_ocg.py | 54 +++-- .../src/main/resources/application.properties | 11 - .../runtime/compiler/ToolCompilerTest.java | 59 +++-- .../ocg/OcgRegisteredTaskDefsTest.java | 36 ---- .../runtime/ocg/OcgRequestTaskTest.java | 204 ------------------ .../runtime/ocg/OcgToolValidatorTest.java | 116 ---------- .../runtime/util/EnrichToolsScriptTest.java | 56 ++++- .../runtime/compiler/AgentCompiler.java | 4 +- .../runtime/compiler/ToolCompiler.java | 65 +----- .../runtime/ocg/OcgCredentialResolver.java | 32 --- .../agentspan/runtime/ocg/OcgProperties.java | 38 ---- .../runtime/ocg/OcgRegisteredTaskDefs.java | 71 ------ .../agentspan/runtime/ocg/OcgRequestTask.java | 200 ----------------- .../runtime/ocg/OcgRequestTaskConfig.java | 93 -------- .../runtime/ocg/OcgToolValidator.java | 141 ------------ .../ocg/PlaceholderCredentialResolver.java | 78 ------- .../operation/OcgCodeHistoryOperation.java | 48 ----- .../ocg/operation/OcgGetEntityOperation.java | 41 ---- .../runtime/ocg/operation/OcgInputs.java | 94 -------- .../operation/OcgMemoryDeleteOperation.java | 46 ---- .../OcgMemoryReinforceOperation.java | 39 ---- .../ocg/operation/OcgMemorySetOperation.java | 44 ---- .../operation/OcgNeighborhoodOperation.java | 48 ----- .../runtime/ocg/operation/OcgOperation.java | 49 ----- .../ocg/operation/OcgQueryOperation.java | 66 ------ .../runtime/ocg/operation/OcgRequest.java | 60 ------ .../runtime/ocg/operation/OcgTarget.java | 28 --- .../runtime/ocg/operation/OcgUri.java | 37 ---- .../runtime/registry/RegisteredTaskDefs.java | 2 +- .../runtime/service/AgentService.java | 16 -- .../runtime/util/JavaScriptBuilder.java | 111 ++++++---- 37 files changed, 340 insertions(+), 1899 deletions(-) delete mode 100644 server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefsTest.java delete mode 100644 server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java delete mode 100644 server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgToolValidatorTest.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgCredentialResolver.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgToolValidator.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/PlaceholderCredentialResolver.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgTarget.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java diff --git a/docs/design/2026-06-12-ocg-sdk-subagent-status.md b/docs/design/2026-06-12-ocg-sdk-subagent-status.md index 1f75ccf20..18255f728 100644 --- a/docs/design/2026-06-12-ocg-sdk-subagent-status.md +++ b/docs/design/2026-06-12-ocg-sdk-subagent-status.md @@ -179,3 +179,20 @@ on `ocg_agent()`/`ocg_tools()`; `OcgProperties` is just `enabled` + `responseCapChars`; `OcgRequestTask`/`OcgToolValidator` have no fallback. SDK, examples, smoke, and docs updated; full server suite + SDK unit tests green; republished to mavenLocal. + +## Addendum (2026-06-12, latest): OCG execution layer deleted — plain HttpTask + +Per user decision, the custom OCG system tasks are gone entirely. The +`runtime/ocg` package (request task, operations, target, validator, +credential resolver, properties, task-def registration) is deleted; the +seven `ocg_*`→`OCG_*` TYPE_MAP entries and all `agentspan.ocg.*` +properties with it. In exchange the generic `http` enrich path gained +optional `pathTemplate`/`queryParams` (URL-encoded fill from LLM args, +consumed args pruned from the body) — proven by GraalJS execution tests. +The SDK's `ocg_tools()` now emits `tool_type="http"` defs carrying +method/pathTemplate/queryParams/headers per operation; auth uses the +standard http-tool `${NAME}` header placeholder. Rationale: the OCG API +already returns LLM-friendly responses, removing the last justification +(projection/capping) for server-side OCG code. Full server suite + SDK +tests green; two-stub e2e 3/3 on the HttpTask path; republished to +mavenLocal. diff --git a/docs/design/2026-06-12-toolspec-selfdescribing-plan.md b/docs/design/2026-06-12-toolspec-selfdescribing-plan.md index a3545d1ad..df4df6de6 100644 --- a/docs/design/2026-06-12-toolspec-selfdescribing-plan.md +++ b/docs/design/2026-06-12-toolspec-selfdescribing-plan.md @@ -94,25 +94,12 @@ marker. ### Change 2 — `server/src/main/resources/application.properties` -Replace the OCG block added in PR #3673 (it documents auto-expose and -requires `OCG_MODEL`, both removed from agentspan). DONE in the local -checkout — the block is now just the enable switch (every OCG tool binds -its own instance from the SDK; there is no server-side instance config): - -```properties -# ============================================================================= -# OCG (Open Context Graph) Configuration -# ============================================================================= -# OCG agents and tools are declared in user code via the AgentSpan SDK -# (ocg_agent() / ocg_tools()), each binding its own OCG instance URL and -# credential-store reference. There is no server-side OCG configuration -# beyond this switch, which registers the OCG_* system tasks. -agentspan.ocg.enabled=${OCG_ENABLED:false} -``` - -(`agentspan.ocg.model`/`url`/`api-key` are removed and ignored by -agentspan. The host defaults the switch to false — set `OCG_ENABLED=true` -in the runtime environment to turn the execution layer on.) +**Delete the OCG block entirely** (whatever its current state — it was +added in PR #3673 and trimmed since). As of the HttpTask refactor, OCG +tools are plain HTTP tools: agentspan reads no `agentspan.ocg.*` property +at all, registers no OCG task types/TaskDefs, and needs no `OCG_ENABLED` +env. Stale `ocg_*`/`query`-style TaskDefs left in the metadata store from +earlier builds are harmless leftovers and may be deleted. ### Tests (fail-first) diff --git a/docs/ocg-agent-flow.md b/docs/ocg-agent-flow.md index 19af74916..83c1e8c82 100644 --- a/docs/ocg-agent-flow.md +++ b/docs/ocg-agent-flow.md @@ -2,11 +2,11 @@ A retrieval sub-agent over the Open Context Graph (OCG) — message search, entity lookup, code history, stored memories — that any agent **opts into -from the SDK**. The SDK is the canonical home of the OCG agent (system -prompt, tool schemas, instance binding); the server provides only the -execution layer: seven `OCG_*` system tasks that make the authenticated -HTTP calls, resolve credentials, project response fields, and cap response -sizes. +from the SDK**. The SDK is the canonical — and only — home of the OCG +integration: system prompt, tool schemas, endpoint routing, and instance +binding. The tools compile to **plain Conductor HTTP tasks** (with path +templating); there is no OCG-specific server code at all. The OCG API +itself returns LLM-friendly responses. Nothing is auto-injected: an agent that doesn't declare OCG tools never makes an OCG call. (The previous design — a server-registered `_ocg_agent` @@ -94,30 +94,10 @@ hook. ## Server setup -The server side is the execution layer only. Configuration -(`application.properties` / env): - -| Property | Env var | Default | What it does | -| --- | --- | --- | --- | -| `agentspan.ocg.enabled` | `OCG_ENABLED` | `true` | Registers the seven `OCG_*` system tasks and `ocg_*` TaskDefs. When `false`, agent starts that declare OCG tools are rejected with a clear error. | -| `agentspan.ocg.response-cap-chars` | — | 8192 | Post-projection truncation cap per response. | - -That's the whole server surface. There is no server-side OCG instance -configuration: no `OCG_URL`/`OCG_API_KEY` (every tool binds its own -instance from the SDK) and no `OCG_MODEL` (the retrieval agent's model is -a required SDK parameter, `ocg_agent(model=...)`). - -### Verifying it's enabled - -```bash -# The seven OCG TaskDefs are registered (names match dispatch): -curl -s localhost:6767/api/metadata/taskdefs \ - | jq '[.[].name | select(startswith("ocg_"))]' -# → ["ocg_query", "ocg_get_entity", ..., "ocg_memory_delete"] -``` - -No workflow is registered at boot — retrieval agents are compiled when a -user agent that declares them starts. +**None.** OCG tools are plain HTTP tools: no properties, no task types, no +TaskDefs, no beans. Any agentspan server (standalone or embedded) that runs +HTTP tools runs OCG tools. Nothing is registered at boot — retrieval agents +are compiled when a user agent that declares them starts. --- @@ -125,58 +105,49 @@ user agent that declares them starts. ``` main agent LLM ── tool call ──▶ SUB_WORKFLOW (retriever) - │ retriever LLM picks e.g. ocg_query + │ retriever LLM picks e.g. ocg_get_entity ▼ - enrich script: t.type = OCG_QUERY, - merges __ocg_url / __ocg_auth from the - tool's compiled instance binding + enrich script (compile-time JS, dispatch-time eval): + uri = + pathTemplate filled from + the LLM's args (URL-encoded) + queryParams + body = remaining args (consumed args removed) + headers carry the credential placeholder ▼ - OcgRequestTask (system task) - 1. target = __ocg_url (required) - 2. auth = __ocg_auth (placeholder resolved - via credential store), absent = no auth - 3. strip reserved inputs, build HTTP request - 4. send → project fields → cap chars + standard Conductor HTTP task ▼ OCG instance ─── citations ──▶ retriever LLM ``` Key properties: -- **Per-call instance binding.** The tool's binding (compiled into the - workflow as the reserved `__ocg_url`/`__ocg_auth` task inputs) is the - only instance. A tool without one is rejected at agent *start* - (`OcgToolValidator`); the task-level check is the backstop. +- **Per-call instance binding.** Each tool's config carries its instance + `url` (required by the SDK) — different agents target different graphs. - **Secrets stay server-side.** `credential="OCG_US_KEY"` compiles to a - placeholder (`#{OCG_US_KEY}` standalone, `${workflow.secrets.OCG_US_KEY}` - embedded). Standalone resolution goes through the credential store scoped - by the execution token (same contract as HTTP tool headers); resolved - values are never written back to the task model. An unresolvable - placeholder fails the task rather than being sent as a bearer token. -- **Response hygiene.** Each operation projects the raw OCG response down - to the fields the LLM needs (e.g. citations), then caps it at - `response-cap-chars` *before* it is persisted or enters the LLM context. + standard HTTP-tool header placeholder (`#{OCG_US_KEY}` standalone, + `${workflow.secrets.OCG_US_KEY}` embedded), resolved from the credential + store at execution — the same pipeline every `http_tool` uses. +- **LLM-friendly responses are the OCG API's job.** The API returns compact + citation-shaped responses; agentspan applies no OCG-specific projection + or capping. ## The seven OCG operations -All endpoints sit under `/api/v1`. Each is backed by a -strategy class implementing `OcgOperation` (under `runtime/ocg/operation/`); -`OcgRequestTask` is a thin orchestrator that resolves the target instance -and delegates URL/method/body/projection to the strategy. - -| Tool name (LLM-visible) | System task type | Endpoint | Method | -| ----------------------- | --------------------- | ---------------------------------------- | -------- | -| `ocg_query` | `OCG_QUERY` | `/api/v1/agent/query` | `POST` | -| `ocg_get_entity` | `OCG_GET_ENTITY` | `/api/v1/entities/{entity_id}` | `GET` | -| `ocg_neighborhood` | `OCG_NEIGHBORHOOD` | `/api/v1/graph/neighborhood/{entity_id}` | `GET` | -| `ocg_code_history` | `OCG_CODE_HISTORY` | `/api/v1/code/history/{repo_id}` | `GET` | -| `ocg_memory_set` | `OCG_MEMORY_SET` | `/api/v1/memories` | `POST` | -| `ocg_memory_reinforce` | `OCG_MEMORY_REINFORCE`| `/api/v1/memories/{key}/reinforce` | `POST` | -| `ocg_memory_delete` | `OCG_MEMORY_DELETE` | `/api/v1/memories/{key}` | `DELETE` | - -The registered TaskDef names are the lowercased task types (`ocg_query`, -…) — Conductor resolves dynamically forked tasks by name, and the dispatch -script names each task `taskType.toLowerCase()`. +Endpoint routing lives in the SDK (`agentspan/agents/ocg.py`) and compiles +into each tool's HTTP config (`pathTemplate` + `queryParams` + method): + +| Tool name (LLM-visible) | Endpoint | Method | +| ----------------------- | ---------------------------------------- | -------- | +| `ocg_query` | `/api/v1/agent/query` | `POST` | +| `ocg_get_entity` | `/api/v1/entities/{entity_id}` | `GET` | +| `ocg_neighborhood` | `/api/v1/graph/neighborhood/{entity_id}` | `GET` | +| `ocg_code_history` | `/api/v1/code/history/{repo_id}` | `GET` | +| `ocg_memory_set` | `/api/v1/memories` | `POST` | +| `ocg_memory_reinforce` | `/api/v1/memories/{key}/reinforce` | `POST` | +| `ocg_memory_delete` | `/api/v1/memories/{key}` | `DELETE` | + +Path params (`{entity_id}`, `{key}`, `{repo_id}`) are filled from the +LLM's tool arguments and URL-encoded by the dispatch script; listed query +params are appended when present; everything else becomes the JSON body. --- @@ -192,7 +163,7 @@ That behavior is removed with no flag and no shim: | `_ocg_agent` workflow registered at boot | No boot-time workflow; retriever compiles when the declaring agent starts | | `OCG_MODEL` required, boot failed fast without it | Model is a required SDK parameter; `OCG_MODEL` is ignored | | One server-wide OCG instance | Per-tool `url=`/`credential=` — required; no server-side instance config at all | -| OCG fully off unless `OCG_URL` set | Execution layer gated by `agentspan.ocg.enabled`; instanceless OCG tools rejected at start | +| Seven `OCG_*` system tasks + TaskDefs + `agentspan.ocg.*` properties | Plain HTTP tasks — zero OCG server code; `agentspan.ocg.*` properties are gone and ignored | Anything that referenced the `_ocg_agent` workflow by name breaks; declare the agent from the SDK instead. diff --git a/docs/python-sdk/api-reference.md b/docs/python-sdk/api-reference.md index cbeaf9b71..f43342a67 100644 --- a/docs/python-sdk/api-reference.md +++ b/docs/python-sdk/api-reference.md @@ -255,10 +255,10 @@ github = mcp_tool( ### ocg_agent() / ocg_tools() — OCG Retrieval Sub-Agent OCG (Open Context Graph) is a retrieval engine over a knowledge graph of -entities (messages, channels, people, code). The SDK is the canonical home of -the OCG retrieval agent — the server provides only the execution layer (the -seven `OCG_*` system tasks that make the HTTP calls, resolve credentials, and -cap responses). OCG is **opt-in per agent**: nothing is auto-injected. +entities (messages, channels, people, code). The SDK is the canonical — and +only — home of the OCG integration: the tools compile to plain Conductor +HTTP tasks (path-templated), so no OCG-specific code exists server-side. +OCG is **opt-in per agent**: nothing is auto-injected. ```python from agentspan.agents import Agent, agent_tool diff --git a/docs/release-notes/2026-06-12-ocg-sdk-subagent.md b/docs/release-notes/2026-06-12-ocg-sdk-subagent.md index 2dafeada2..f6181e51e 100644 --- a/docs/release-notes/2026-06-12-ocg-sdk-subagent.md +++ b/docs/release-notes/2026-06-12-ocg-sdk-subagent.md @@ -27,11 +27,14 @@ data residency). The server keeps only the execution layer. retrieval agent's model is a required SDK parameter. The boot-time fail-fast tied to it is gone. -4. **Gating changed; server-side instance config removed.** The `OCG_*` - system tasks are registered when `agentspan.ocg.enabled=true` (the - default) — no longer conditional on `OCG_URL`. `agentspan.ocg.url` / - `api-key` are **gone**: every OCG tool binds its own instance - (`url=`, required) from the SDK; there is no server-wide default. +4. **All OCG server code removed.** OCG tools compile to plain Conductor + HTTP tasks (with path templating, a new generally-available `http` + tool capability). The seven `OCG_*` system task types, their TaskDefs, + and every `agentspan.ocg.*` property (`enabled`/`url`/`api-key`/ + `model`/`response-cap-chars`) are gone and ignored. Every OCG tool + binds its own instance (`url=`, required) from the SDK; the OCG API + itself returns LLM-friendly responses (no server-side projection or + capping). ## New @@ -41,9 +44,11 @@ data residency). The server keeps only the execution layer. is required; the credential is a credential-store *name*, resolved server-side at execution; secrets never enter Python code or workflow definitions. -- Start-time validation: an OCG tool without a bound `url` is rejected at - agent start (not mid-conversation), as is any OCG tool when - `agentspan.ocg.enabled=false`. +- HTTP tool path templating: `http`-type tool configs may declare + `pathTemplate` (e.g. `/api/v1/entities/{entity_id}`) and `queryParams`; + the dispatch script fills them from the LLM's arguments (URL-encoded) + and prunes consumed args from the body. OCG uses this; any HTTP tool + can. - Every compiled tool spec now carries a `selfDescribing: true` marker — top-level and inside `configParams` (the copy that survives `ToolSpec` deserialization) — consumed by embedding hosts (orkes-conductor's diff --git a/sdk/python/src/agentspan/agents/ocg.py b/sdk/python/src/agentspan/agents/ocg.py index a931d15f3..5e828bb48 100644 --- a/sdk/python/src/agentspan/agents/ocg.py +++ b/sdk/python/src/agentspan/agents/ocg.py @@ -5,10 +5,10 @@ OCG is a retrieval engine over a knowledge graph of entities (messages, channels, people, code) linked by claims and relationships. This module is -the canonical definition of the OCG retrieval agent — system prompt, tool -schemas, and instance binding all live here; the server provides only the -execution layer (the seven ``OCG_*`` system tasks that make the HTTP calls, -resolve credentials, and cap responses). +the canonical — and only — definition of the OCG integration: system +prompt, tool schemas, endpoint routing, and instance binding all live +here. The tools compile to plain Conductor HTTP tasks (with path +templating); there is no OCG-specific server code at all. Typical usage — delegate retrieval from a main agent:: @@ -138,6 +138,8 @@ def _object(properties: Dict[str, Any], required: List[str]) -> Dict[str, Any]: def _query_tool() -> Dict[str, Any]: return { "name": "ocg_query", + "method": "POST", + "path": "/api/v1/agent/query", "description": ( "Query the Open Context Graph for structured retrieval. " "Returns citations (source_item_id, title, container_id, snippet) " @@ -161,6 +163,8 @@ def _query_tool() -> Dict[str, Any]: def _get_entity_tool() -> Dict[str, Any]: return { "name": "ocg_get_entity", + "method": "GET", + "path": "/api/v1/entities/{entity_id}", "description": "Fetch one entity by its canonical id.", "schema": _object( {"entity_id": _prop("string", "Canonical entity id from an ocg_query result row.")}, @@ -172,6 +176,9 @@ def _get_entity_tool() -> Dict[str, Any]: def _neighborhood_tool() -> Dict[str, Any]: return { "name": "ocg_neighborhood", + "method": "GET", + "path": "/api/v1/graph/neighborhood/{entity_id}", + "query_params": ["depth", "limit"], "description": ( "Get an entity plus its graph neighbors out to `depth` hops. " "Use limit <= 10, depth=1 on the first call — well-connected " @@ -193,6 +200,9 @@ def _neighborhood_tool() -> Dict[str, Any]: def _code_history_tool() -> Dict[str, Any]: return { "name": "ocg_code_history", + "method": "GET", + "path": "/api/v1/code/history/{repo_id}", + "query_params": ["path", "limit"], "description": "Last N commits that touched a file in an ingested repo.", "schema": _object( { @@ -208,6 +218,8 @@ def _code_history_tool() -> Dict[str, Any]: def _memory_set_tool() -> Dict[str, Any]: return { "name": "ocg_memory_set", + "method": "POST", + "path": "/api/v1/memories", "description": ( "Create or overwrite a memory in OCG. Cap inferred confidence at 0.7; " "never write PII or secrets." @@ -240,6 +252,8 @@ def _memory_set_tool() -> Dict[str, Any]: def _memory_reinforce_tool() -> Dict[str, Any]: return { "name": "ocg_memory_reinforce", + "method": "POST", + "path": "/api/v1/memories/{key}/reinforce", "description": ( "Reinforce an existing memory on independent re-observation. confidence_boost must be <= 0.05." ), @@ -261,6 +275,9 @@ def _memory_reinforce_tool() -> Dict[str, Any]: def _memory_delete_tool() -> Dict[str, Any]: return { "name": "ocg_memory_delete", + "method": "DELETE", + "path": "/api/v1/memories/{key}", + "query_params": ["agent", "user"], "description": ( "Delete a memory by key. Prefer ocg_memory_set with a corrected value " "over deletion (preserves history)." @@ -290,9 +307,10 @@ def ocg_tools( ) -> List[ToolDef]: """Build the raw OCG :class:`ToolDef` list for a custom retrieval agent. - Each tool dispatches to the matching ``OCG_*`` system task on the - server, which owns the HTTP call, credential resolution, field - projection, and response capping. + Each tool is a plain Conductor HTTP task: the compile bakes the + instance URL, endpoint path template, and auth header into the tool's + dispatch config, and the LLM's arguments fill the path/query/body at + call time. No OCG-specific code runs server-side. Args: url: Base URL of the OCG instance this tool set targets. Required — @@ -314,10 +332,13 @@ def ocg_tools( "ocg_tools() requires a non-blank url: every OCG tool set binds its own instance." ) - config: Dict[str, Any] = {"url": url} + base_url = url.strip().rstrip("/") + headers: Dict[str, str] = {} credentials: List[str] = [] if credential: - config["credential"] = credential + # Standard http-tool placeholder — resolved server-side from the + # credential store at execution; the token never appears here. + headers["Authorization"] = "Bearer ${" + credential + "}" credentials = [credential] selected: List[Dict[str, Any]] = [] @@ -333,17 +354,28 @@ def ocg_tools( selected.append(_memory_reinforce_tool()) selected.append(_memory_delete_tool()) - return [ - ToolDef( - name=spec["name"], - description=spec["description"], - input_schema=spec["schema"], - tool_type=spec["name"], - config=dict(config), - credentials=list(credentials), + tools: List[ToolDef] = [] + for spec in selected: + config: Dict[str, Any] = { + "url": base_url, + "method": spec["method"], + "pathTemplate": spec["path"], + } + if spec.get("query_params"): + config["queryParams"] = list(spec["query_params"]) + if headers: + config["headers"] = dict(headers) + tools.append( + ToolDef( + name=spec["name"], + description=spec["description"], + input_schema=spec["schema"], + tool_type="http", + config=config, + credentials=list(credentials), + ) ) - for spec in selected - ] + return tools def ocg_agent( diff --git a/sdk/python/tests/unit/test_ocg.py b/sdk/python/tests/unit/test_ocg.py index a00c2264f..2409e600e 100644 --- a/sdk/python/tests/unit/test_ocg.py +++ b/sdk/python/tests/unit/test_ocg.py @@ -8,7 +8,7 @@ from agentspan.agents.ocg import OCG_SYSTEM_PROMPT, ocg_agent, ocg_tools from agentspan.agents.tool import ToolDef -ALL_TOOL_TYPES = { +ALL_TOOL_NAMES = { "ocg_query", "ocg_get_entity", "ocg_neighborhood", @@ -26,16 +26,16 @@ class TestOcgTools: def test_default_returns_all_seven(self): tools = ocg_tools(url=URL) assert len(tools) == 7 - assert {t.tool_type for t in tools} == ALL_TOOL_TYPES - # Name matches tool_type for every OCG tool (the server keys - # ocgConfig by tool name). - assert all(t.name == t.tool_type for t in tools) + assert {t.name for t in tools} == ALL_TOOL_NAMES + # OCG tools ARE http tools — they execute as plain Conductor HTTP + # tasks; there is no OCG-specific server code. + assert all(t.tool_type == "http" for t in tools) assert all(isinstance(t, ToolDef) for t in tools) def test_memory_false_returns_retrieval_only(self): tools = ocg_tools(url=URL, memory=False) assert len(tools) == 4 - assert {t.tool_type for t in tools} == { + assert {t.name for t in tools} == { "ocg_query", "ocg_get_entity", "ocg_neighborhood", @@ -44,7 +44,7 @@ def test_memory_false_returns_retrieval_only(self): def test_subset_switches(self): tools = ocg_tools(url=URL, entities=False, code_history=False, memory=False) - assert [t.tool_type for t in tools] == ["ocg_query"] + assert [t.name for t in tools] == ["ocg_query"] def test_url_is_required(self): # There is no server-side default instance — every OCG tool set @@ -58,15 +58,40 @@ def test_instance_binding_lands_in_config(self): tools = ocg_tools(url=URL, credential="OCG_US_KEY") for t in tools: assert t.config["url"] == URL - assert t.config["credential"] == "OCG_US_KEY" + # Auth rides a standard http-tool header placeholder; the server + # resolves ${NAME} from the credential store at execution. + assert t.config["headers"] == {"Authorization": "Bearer ${OCG_US_KEY}"} # Declared so the execution token bounds credential resolution # (same wire contract as http_tool headers). assert t.credentials == ["OCG_US_KEY"] + def test_endpoint_mapping(self): + by_name = {t.name: t for t in ocg_tools(url=URL)} + q = by_name["ocg_query"].config + assert (q["method"], q["pathTemplate"]) == ("POST", "/api/v1/agent/query") + e = by_name["ocg_get_entity"].config + assert (e["method"], e["pathTemplate"]) == ("GET", "/api/v1/entities/{entity_id}") + n = by_name["ocg_neighborhood"].config + assert n["pathTemplate"] == "/api/v1/graph/neighborhood/{entity_id}" + assert n["queryParams"] == ["depth", "limit"] + c = by_name["ocg_code_history"].config + assert c["pathTemplate"] == "/api/v1/code/history/{repo_id}" + assert c["queryParams"] == ["path", "limit"] + r = by_name["ocg_memory_reinforce"].config + assert (r["method"], r["pathTemplate"]) == ("POST", "/api/v1/memories/{key}/reinforce") + d = by_name["ocg_memory_delete"].config + assert (d["method"], d["pathTemplate"]) == ("DELETE", "/api/v1/memories/{key}") + assert d["queryParams"] == ["agent", "user"] + + def test_trailing_slash_stripped_from_url(self): + tools = ocg_tools(url="https://us.ocg.example.com/") + assert all(t.config["url"] == "https://us.ocg.example.com" for t in tools) + def test_url_without_credential_is_allowed(self): tools = ocg_tools(url="https://local-ocg:8080") for t in tools: - assert t.config == {"url": "https://local-ocg:8080"} + assert t.config["url"] == "https://local-ocg:8080" + assert "headers" not in t.config assert t.credentials == [] def test_credential_without_url_raises(self): @@ -74,7 +99,7 @@ def test_credential_without_url_raises(self): ocg_tools(credential="OCG_US_KEY") def test_schemas_have_required_fields(self): - by_type = {t.tool_type: t for t in ocg_tools(url=URL)} + by_type = {t.name: t for t in ocg_tools(url=URL)} assert by_type["ocg_query"].input_schema["required"] == ["query"] assert by_type["ocg_get_entity"].input_schema["required"] == ["entity_id"] assert by_type["ocg_code_history"].input_schema["required"] == ["repo_id", "path"] @@ -132,7 +157,8 @@ def test_instance_binding_flows_to_tools(self): assert len(tool_defs) == 7 for td in tool_defs: assert td.config["url"] == "https://us.ocg.example.com" - assert td.config["credential"] == "OCG_US_KEY" + assert td.config["headers"] == {"Authorization": "Bearer ${OCG_US_KEY}"} + assert td.credentials == ["OCG_US_KEY"] def test_tool_subset_flags_forwarded(self): agent = ocg_agent(model="openai/gpt-4o-mini", url=URL, memory=False) @@ -174,7 +200,9 @@ def test_serializes_with_instance_config(self): child = at["config"]["agentConfig"] assert child["name"] == "ocg_us" ocg_query = next(t for t in child["tools"] if t["name"] == "ocg_query") - assert ocg_query["toolType"] == "ocg_query" + # OCG tools are plain http tools on the wire. + assert ocg_query["toolType"] == "http" assert ocg_query["config"]["url"] == "https://us.ocg.example.com" - assert ocg_query["config"]["credential"] == "OCG_US_KEY" + assert ocg_query["config"]["pathTemplate"] == "/api/v1/agent/query" + assert ocg_query["config"]["headers"] == {"Authorization": "Bearer ${OCG_US_KEY}"} assert ocg_query["config"]["credentials"] == ["OCG_US_KEY"] diff --git a/server/conductor-agentspan-server/src/main/resources/application.properties b/server/conductor-agentspan-server/src/main/resources/application.properties index 9e346f229..4e227def7 100644 --- a/server/conductor-agentspan-server/src/main/resources/application.properties +++ b/server/conductor-agentspan-server/src/main/resources/application.properties @@ -165,17 +165,6 @@ agentspan.credentials.resolve.rate-limit=120 # spring.sql.init.mode=always # spring.sql.init.schema-locations=classpath:schema-secrets.sql -# ============================================================================= -# OCG (Open Context Graph) Configuration -# ============================================================================= -# OCG agents and tools are declared in user code via the AgentSpan SDK -# (ocg_agent() / ocg_tools()), each binding its own OCG instance URL and -# credential-store reference. There is no server-side OCG instance -# configuration — this switch only registers the 7 OCG_* system tasks. -agentspan.ocg.enabled=${OCG_ENABLED:true} -# Per-call response cap defaults to 8192 in OcgProperties. Uncomment to override. -# agentspan.ocg.response-cap-chars=8192 - # Metrics conductor.metrics-prometheus.enabled=true management.endpoints.web.exposure.include=health,info,prometheus diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/ToolCompilerTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/ToolCompilerTest.java index fac5b64a5..959d2ea1a 100644 --- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/ToolCompilerTest.java +++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/compiler/ToolCompilerTest.java @@ -355,9 +355,10 @@ void testCompileToolSpecs_everySpecIsSelfDescribing() { .config(Map.of("url", "https://example.com")) .build(), ToolConfig.builder() - .name("ocg_query") - .description("Query OCG") - .toolType("ocg_query") + .name("github") + .description("GitHub MCP") + .toolType("mcp") + .config(Map.of("server_url", "http://localhost:3001/mcp")) .build(), ToolConfig.builder() .name("helper") @@ -405,44 +406,56 @@ void testCompileToolSpecs_markerMergesIntoExistingConfigParams() { assertThat(cp.get("selfDescribing")).isEqualTo(Boolean.TRUE); } - // ── OCG per-instance config plumbing ───────────────────────────────── + // ── HTTP path templating (used by OCG tools, generally available) ──── @Test - void testBuildEnrichTask_ocgInstanceConfig() { - // An OCG tool bound to a specific instance carries its url and a - // bearer-credential placeholder into the baked ocgCfg, and the script - // merges them into the dispatched task input as __ocg_url/__ocg_auth. + void testBuildEnrichTask_httpPathTemplate() { + // An http tool may declare a pathTemplate + queryParams: the enrich + // script builds the URI from the LLM's arguments at dispatch time + // (URL-encoded), consumed args are excluded from the body, and + // ${NAME} header placeholders are escaped for the credential + // resolver. This is how OCG tools compile — plain HTTP tasks. ToolConfig tool = ToolConfig.builder() - .name("ocg_query") - .description("Query the US graph") - .toolType("ocg_query") - .config(Map.of("url", "https://us.ocg.example.com", "credential", "OCG_US_KEY")) + .name("ocg_get_entity") + .description("Fetch one entity") + .toolType("http") + .config(Map.of( + "url", "https://us.ocg.example.com", + "method", "GET", + "pathTemplate", "/api/v1/entities/{entity_id}", + "queryParams", List.of("depth", "limit"), + "headers", Map.of("Authorization", "Bearer ${OCG_US_KEY}"))) .build(); Object[] result = new ToolCompiler().buildEnrichTask("agent", "agent_llm", List.of(tool), ""); String script = (String) ((WorkflowTask) result[0]).getInputParameters().get("expression"); assertThat(script).contains("\"url\":\"https://us.ocg.example.com\""); - // Standalone mode: ${OCG_US_KEY} is escaped to #{OCG_US_KEY} so + assertThat(script).contains("\"pathTemplate\":\"/api/v1/entities/{entity_id}\""); + assertThat(script).contains("\"queryParams\":[\"depth\",\"limit\"]"); + // Standalone mode: ${OCG_US_KEY} escaped to #{OCG_US_KEY} so // Conductor's parameter binding doesn't consume it. - assertThat(script).contains("\"auth\":\"Bearer #{OCG_US_KEY}\""); - assertThat(script).contains("__ocg_url"); - assertThat(script).contains("__ocg_auth"); + assertThat(script).contains("Bearer #{OCG_US_KEY}"); + // The templating machinery itself must be in the script. + assertThat(script).contains("pathTemplate"); + assertThat(script).contains("encodeURIComponent"); } @Test - void testBuildEnrichTask_ocgDefaultInstance() { - // No url/credential in config → the baked entry carries only the - // task type; the system task falls back to the server default. + void testBuildEnrichTask_plainHttpToolUnchanged() { + // http tools without templating keys keep the existing static-uri, + // args-as-body shape — no behavior change for the established path. ToolConfig tool = ToolConfig.builder() - .name("ocg_query") - .description("Query OCG") - .toolType("ocg_query") + .name("weather") + .description("Get weather") + .toolType("http") + .config(Map.of("url", "https://api.weather.com", "method", "POST")) .build(); Object[] result = new ToolCompiler().buildEnrichTask("agent", "agent_llm", List.of(tool), ""); String script = (String) ((WorkflowTask) result[0]).getInputParameters().get("expression"); - assertThat(script).contains("\"ocg_query\":{\"taskType\":\"OCG_QUERY\"}"); + assertThat(script).contains("\"url\":\"https://api.weather.com\""); + assertThat(script).doesNotContain("\"weather\":{\"pathTemplate\""); } } diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefsTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefsTest.java deleted file mode 100644 index 1e1dfae1f..000000000 --- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefsTest.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import static org.assertj.core.api.Assertions.*; - -import java.util.List; - -import org.junit.jupiter.api.Test; - -class OcgRegisteredTaskDefsTest { - - @Test - void taskDefNamesMatchDispatchedTaskNames() { - // The enrich script schedules each OCG tool call as a task named - // ``taskType.toLowerCase()`` (e.g. OCG_QUERY → "ocg_query"). Conductor - // resolves dynamic-fork tasks by NAME, so the registered TaskDefs must - // use exactly those names — anything else fails dispatch with - // "Cannot find task by name ocg_query in the task definitions". - List names = new OcgRegisteredTaskDefs() - .taskDefs().stream().map(def -> def.getName()).toList(); - - assertThat(names) - .containsExactlyInAnyOrder( - "ocg_query", - "ocg_get_entity", - "ocg_neighborhood", - "ocg_code_history", - "ocg_memory_set", - "ocg_memory_reinforce", - "ocg_memory_delete"); - } -} diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java deleted file mode 100644 index d2cf6cd10..000000000 --- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgRequestTaskTest.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.HashMap; -import java.util.Map; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import com.netflix.conductor.model.TaskModel; -import com.netflix.conductor.model.WorkflowModel; - -import dev.agentspan.runtime.ocg.operation.OcgQueryOperation; - -/** - * Per-call OCG instance resolution: the tool-bound instance - * ({@code __ocg_url} / {@code __ocg_auth} task inputs) is the only - * instance — there is no server-side default. A task dispatched without - * {@code __ocg_url} fails fast. - */ -class OcgRequestTaskTest { - - private HttpClient httpClient; - private HttpResponse response; - - @BeforeEach - @SuppressWarnings("unchecked") - void setUp() throws Exception { - httpClient = mock(HttpClient.class); - response = mock(HttpResponse.class); - when(response.statusCode()).thenReturn(200); - when(response.body()).thenReturn("{\"citations\":[]}"); - when(httpClient.send(any(), any(HttpResponse.BodyHandler.class))).thenReturn(response); - } - - private static OcgProperties props() { - return new OcgProperties(); - } - - private static TaskModel taskWithInput(Map input) { - TaskModel task = new TaskModel(); - task.setInputData(new HashMap<>(input)); - return task; - } - - private HttpRequest sentRequest() throws Exception { - ArgumentCaptor captor = ArgumentCaptor.forClass(HttpRequest.class); - verify(httpClient).send(captor.capture(), any()); - return captor.getValue(); - } - - @Test - void perToolUrlIsUsed() throws Exception { - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(), httpClient, null); - TaskModel model = taskWithInput(Map.of("query", "hello", "__ocg_url", "https://ca.ocg.example.com")); - - task.start(new WorkflowModel(), model, null); - - assertThat(model.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); - assertThat(sentRequest().uri().toString()).startsWith("https://ca.ocg.example.com/api/v1/"); - } - - @Test - void failsFastWithoutBoundInstance() { - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(), httpClient, null); - TaskModel model = taskWithInput(Map.of("query", "hello")); - - task.start(new WorkflowModel(), model, null); - - assertThat(model.getStatus()).isEqualTo(TaskModel.Status.FAILED); - assertThat(model.getReasonForIncompletion()).contains("no OCG instance").contains("url="); - verifyNoInteractions(httpClient); - } - - @Test - void preResolvedAuthHeaderIsSentVerbatim() throws Exception { - // Embedded mode: the host already substituted ${workflow.secrets.NAME}, - // so __ocg_auth arrives fully resolved. - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(), httpClient, null); - TaskModel model = taskWithInput(Map.of( - "query", "hello", - "__ocg_url", "https://us.ocg.example.com", - "__ocg_auth", "Bearer resolved-us-secret")); - - task.start(new WorkflowModel(), model, null); - - assertThat(model.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); - assertThat(sentRequest().headers().firstValue("Authorization")).hasValue("Bearer resolved-us-secret"); - } - - @Test - void placeholderAuthIsResolvedThroughCredentialResolver() throws Exception { - OcgCredentialResolver resolver = (value, ctx) -> value.replace("#{OCG_US_KEY}", "us-secret"); - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(), httpClient, resolver); - TaskModel model = taskWithInput(Map.of( - "query", "hello", - "__ocg_url", "https://us.ocg.example.com", - "__ocg_auth", "Bearer #{OCG_US_KEY}", - "__agentspan_ctx__", Map.of("execution_token", "tok"))); - - task.start(new WorkflowModel(), model, null); - - assertThat(model.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); - assertThat(sentRequest().headers().firstValue("Authorization")).hasValue("Bearer us-secret"); - } - - @Test - void unresolvablePlaceholderFailsTheTask() { - // Resolver returns null (unknown credential / invalid token): the task - // must fail rather than send the placeholder as a bearer token. - OcgCredentialResolver resolver = (value, ctx) -> null; - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(), httpClient, resolver); - TaskModel model = taskWithInput(Map.of( - "query", "hello", - "__ocg_url", "https://us.ocg.example.com", - "__ocg_auth", "Bearer #{OCG_US_KEY}")); - - task.start(new WorkflowModel(), model, null); - - assertThat(model.getStatus()).isEqualTo(TaskModel.Status.FAILED); - assertThat(model.getReasonForIncompletion()).contains("credential"); - verifyNoInteractions(httpClient); - } - - @Test - void noAuthHeaderWhenNoPerToolCredential() throws Exception { - // No credential bound → unauthenticated call; there is no server-side - // default key to silently attach. - OcgRequestTask task = new OcgRequestTask(new OcgQueryOperation(), props(), httpClient, null); - TaskModel model = taskWithInput(Map.of("query", "hello", "__ocg_url", "https://us.ocg.example.com")); - - task.start(new WorkflowModel(), model, null); - - assertThat(model.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); - assertThat(sentRequest().headers().firstValue("Authorization")).isEmpty(); - } - - @Test - void reservedInputsAreNotForwardedToTheOcgApi() throws Exception { - // OcgMemorySetOperation posts the whole input map as the request body — - // the instance-binding keys must never leak into it. - OcgRequestTask task = new OcgRequestTask( - new dev.agentspan.runtime.ocg.operation.OcgMemorySetOperation(), props(), httpClient, null); - TaskModel model = taskWithInput(Map.of( - "key", - "k", - "agent", - "a", - "user", - "u", - "string_value", - "v", - "description", - "d", - "__ocg_url", - "https://us.ocg.example.com", - "__ocg_auth", - "Bearer resolved")); - - task.start(new WorkflowModel(), model, null); - - assertThat(model.getStatus()).isEqualTo(TaskModel.Status.COMPLETED); - HttpRequest sent = sentRequest(); - String body = sent.bodyPublisher() - .map(p -> { - var collector = java.net.http.HttpResponse.BodySubscribers.ofString( - java.nio.charset.StandardCharsets.UTF_8); - p.subscribe(new java.util.concurrent.Flow.Subscriber<>() { - public void onSubscribe(java.util.concurrent.Flow.Subscription s) { - collector.onSubscribe(s); - s.request(Long.MAX_VALUE); - } - - public void onNext(java.nio.ByteBuffer item) { - collector.onNext(java.util.List.of(item)); - } - - public void onError(Throwable t) { - collector.onError(t); - } - - public void onComplete() { - collector.onComplete(); - } - }); - return collector.getBody().toCompletableFuture().join(); - }) - .orElse(""); - assertThat(body).doesNotContain("__ocg_url").doesNotContain("__ocg_auth"); - assertThat(body).contains("\"key\":\"k\""); - } -} diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgToolValidatorTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgToolValidatorTest.java deleted file mode 100644 index 77960f9fd..000000000 --- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ocg/OcgToolValidatorTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import static org.assertj.core.api.Assertions.*; - -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import org.junit.jupiter.api.Test; - -import dev.agentspan.runtime.model.AgentConfig; -import dev.agentspan.runtime.model.ToolConfig; - -class OcgToolValidatorTest { - - private static OcgProperties enabled() { - return new OcgProperties(); - } - - private static ToolConfig ocgTool(Map config) { - return ToolConfig.builder() - .name("ocg_query") - .toolType("ocg_query") - .description("Query OCG") - .config(config) - .build(); - } - - private static AgentConfig agentWith(ToolConfig... tools) { - return AgentConfig.builder() - .name("main") - .model("openai/gpt-4o") - .tools(List.of(tools)) - .build(); - } - - @Test - void ocgToolWithPerToolUrlIsValid() { - AgentConfig config = agentWith(ocgTool(Map.of("url", "https://us.ocg.example.com"))); - - assertThat(OcgToolValidator.validate(config, enabled())).isEmpty(); - } - - @Test - void ocgToolWithoutUrlIsRejected() { - // No server-side default instance exists — every OCG tool must bind - // its own url. - AgentConfig config = agentWith(ocgTool(null)); - - Optional error = OcgToolValidator.validate(config, enabled()); - - assertThat(error).isPresent(); - assertThat(error.get()).contains("ocg_query").contains("url="); - } - - @Test - void ocgToolIsRejectedWhenFeatureDisabled() { - AgentConfig config = agentWith(ocgTool(Map.of("url", "https://us.ocg.example.com"))); - - Optional error = OcgToolValidator.validate(config, null); - - assertThat(error).isPresent(); - assertThat(error.get()).contains("agentspan.ocg.enabled"); - } - - @Test - void nonOcgToolsAreIgnored() { - AgentConfig config = agentWith(ToolConfig.builder() - .name("fetch") - .toolType("http") - .config(Map.of("url", "https://example.com")) - .build()); - - assertThat(OcgToolValidator.validate(config, null)).isEmpty(); - } - - @Test - void instancelessOcgToolInsideInlineAgentToolChildIsRejected() { - // The SDK serializes agent_tool children as raw maps under - // config.agentConfig — the validator must walk that shape too. - Map childAgent = Map.of( - "name", - "retriever", - "tools", - List.of(Map.of( - "name", "ocg_query", - "toolType", "ocg_query"))); - ToolConfig agentTool = ToolConfig.builder() - .name("retriever") - .toolType("agent_tool") - .config(Map.of("agentConfig", childAgent)) - .build(); - - Optional error = OcgToolValidator.validate(agentWith(agentTool), enabled()); - - assertThat(error).isPresent(); - assertThat(error.get()).contains("ocg_query"); - } - - @Test - void instancelessOcgToolInSubAgentIsRejected() { - AgentConfig sub = agentWith(ocgTool(null)); - AgentConfig main = AgentConfig.builder() - .name("main") - .model("openai/gpt-4o") - .agents(List.of(sub)) - .build(); - - assertThat(OcgToolValidator.validate(main, enabled())).isPresent(); - } -} diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java index c3cc93e85..3f97bfbc6 100644 --- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java +++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java @@ -43,14 +43,19 @@ void tearDown() { private List> enrich(String knownNamesJson, String toolCallsJson) throws Exception { // All optional config maps are empty so every name falls through to the // generic SIMPLE-or-unknown branch. That's the path the harness uses. - return enrichWithAgentTools("{}", knownNamesJson, toolCallsJson); + return enrichWithConfigs("{}", "{}", knownNamesJson, toolCallsJson); } - @SuppressWarnings("unchecked") private List> enrichWithAgentTools( String agentToolJson, String knownNamesJson, String toolCallsJson) throws Exception { + return enrichWithConfigs("{}", agentToolJson, knownNamesJson, toolCallsJson); + } + + @SuppressWarnings("unchecked") + private List> enrichWithConfigs( + String httpJson, String agentToolJson, String knownNamesJson, String toolCallsJson) throws Exception { String script = JavaScriptBuilder.enrichToolsScript( - "{}", "{}", "{}", agentToolJson, "{}", "{}", "{}", "{}", "{}", knownNamesJson); + httpJson, "{}", "{}", agentToolJson, "{}", "{}", "{}", "{}", knownNamesJson); // Wrap so the script's IIFE return is captured AND we get a JSON string // back — Graal's Value.toString() is JS source, not JSON. String wrapped = "var $ = {" @@ -202,4 +207,49 @@ void mixedKnownAndUnknownInOneTurn() throws Exception { assertThat(tasks.get(0).get("type")).isEqualTo("SIMPLE"); assertThat(tasks.get(1).get("type")).isEqualTo("INLINE"); } + + @Test + @SuppressWarnings("unchecked") + void httpPathTemplateBuildsUriAndPrunesBody() throws Exception { + // OCG-style tool: GET with a path param and query params. The script + // must URL-encode args into the URI and drop consumed args from the + // body. + String httpCfg = "{\"ocg_get_entity\": {" + + "\"url\": \"https://us.ocg.example.com\"," + + "\"method\": \"GET\"," + + "\"pathTemplate\": \"/api/v1/entities/{entity_id}\"," + + "\"queryParams\": [\"depth\", \"limit\"]," + + "\"headers\": {\"Authorization\": \"Bearer #{OCG_US_KEY}\"}}}"; + String toolCalls = "[{\"name\": \"ocg_get_entity\", \"taskReferenceName\": \"call_1\"," + + " \"inputParameters\": {\"entity_id\": \"entity_01/AB C\", \"depth\": 2}}]"; + + List> tasks = enrichWithConfigs(httpCfg, "{}", "{\"ocg_get_entity\": true}", toolCalls); + + assertThat(tasks).hasSize(1); + Map task = tasks.get(0); + assertThat(task.get("type")).isEqualTo("HTTP"); + Map req = + (Map) ((Map) task.get("inputParameters")).get("http_request"); + assertThat(req.get("uri")).isEqualTo("https://us.ocg.example.com/api/v1/entities/entity_01%2FAB%20C?depth=2"); + assertThat(req.get("method")).isEqualTo("GET"); + assertThat((Map) req.get("headers")).containsEntry("Authorization", "Bearer #{OCG_US_KEY}"); + // entity_id and depth were consumed; limit was never supplied. + assertThat((Map) req.get("body")).isEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + void httpWithoutTemplateKeepsArgsAsBody() throws Exception { + // Established http_tool shape: static uri, all args as body. + String httpCfg = "{\"weather\": {\"url\": \"https://api.weather.com\", \"method\": \"POST\"}}"; + String toolCalls = "[{\"name\": \"weather\", \"taskReferenceName\": \"call_1\"," + + " \"inputParameters\": {\"city\": \"SF\"}}]"; + + List> tasks = enrichWithConfigs(httpCfg, "{}", "{\"weather\": true}", toolCalls); + + Map req = + (Map) ((Map) tasks.get(0).get("inputParameters")).get("http_request"); + assertThat(req.get("uri")).isEqualTo("https://api.weather.com"); + assertThat((Map) req.get("body")).containsEntry("city", "SF"); + } } diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java index 81f7bad5d..3ee512503 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java @@ -91,8 +91,8 @@ String getText() { /** * Public entry point: compile an {@link AgentConfig} into a * {@link WorkflowDef}. An agent's compiled tool list is exactly its - * declared tool list — server-side capabilities (e.g. OCG) are opted - * into explicitly from the SDK, never injected here. + * declared tool list — capabilities are opted into explicitly from + * the SDK, never injected here. */ public WorkflowDef compile(AgentConfig config) { WorkflowDef wf; diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java index 201a8a422..80553608a 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java @@ -104,21 +104,6 @@ private static Map escapeHeadersInConfig(Map cfg /** RAG tool types that map to Conductor RAG system tasks. */ private static final Set RAG_TOOL_TYPES = Set.of("rag_index", "rag_search"); - /** - * OCG tool types — each maps to a {@code OCG_*} {@code WorkflowSystemTask} - * registered by {@code OcgRequestTaskConfig}. Compile-time routing is the - * same as RAG/media: the tool name keys into ``ocgConfig`` at runtime, and - * the enrich script sets ``t.type`` to the configured task type. - */ - public static final Set OCG_TOOL_TYPES = Set.of( - "ocg_query", - "ocg_get_entity", - "ocg_neighborhood", - "ocg_code_history", - "ocg_memory_set", - "ocg_memory_reinforce", - "ocg_memory_delete"); - /** Maps SDK tool type strings to Conductor task type strings. */ private static final Map TYPE_MAP = Map.ofEntries( Map.entry("worker", "SIMPLE"), @@ -132,14 +117,7 @@ private static Map escapeHeadersInConfig(Map cfg Map.entry("generate_video", "GENERATE_VIDEO"), Map.entry("rag_index", "LLM_INDEX_TEXT"), Map.entry("rag_search", "LLM_SEARCH_INDEX"), - Map.entry("pull_workflow_messages", "PULL_WORKFLOW_MESSAGES"), - Map.entry("ocg_query", "OCG_QUERY"), - Map.entry("ocg_get_entity", "OCG_GET_ENTITY"), - Map.entry("ocg_neighborhood", "OCG_NEIGHBORHOOD"), - Map.entry("ocg_code_history", "OCG_CODE_HISTORY"), - Map.entry("ocg_memory_set", "OCG_MEMORY_SET"), - Map.entry("ocg_memory_reinforce", "OCG_MEMORY_REINFORCE"), - Map.entry("ocg_memory_delete", "OCG_MEMORY_DELETE")); + Map.entry("pull_workflow_messages", "PULL_WORKFLOW_MESSAGES")); // ── Public API ─────────────────────────────────────────────────────── @@ -336,7 +314,6 @@ public Object[] buildEnrichTask(String agentName, String llmRef, List cliConfig = new LinkedHashMap<>(); Map humanConfig = new LinkedHashMap<>(); Map wmqConfig = new LinkedHashMap<>(); - Map ocgConfig = new LinkedHashMap<>(); if (tools != null) { Set serverSideTypes = new HashSet<>(Set.of( @@ -352,7 +329,6 @@ public Object[] buildEnrichTask(String agentName, String llmRef, List wmqEntry = new LinkedHashMap<>(); wmqEntry.put("batchSize", cfg.getOrDefault("batchSize", 1)); wmqConfig.put(tool.getName(), wmqEntry); - } else if (OCG_TOOL_TYPES.contains(toolType)) { - // OCG tools map to per-operation system tasks registered by - // OcgRequestTaskConfig. A tool may bind its own OCG instance - // (url + credential-store name); tools without one fall back - // to the server default in OcgProperties at execution time. - Map ocgEntry = new LinkedHashMap<>(); - ocgEntry.put("taskType", TYPE_MAP.getOrDefault(toolType, toolType.toUpperCase())); - Object ocgUrl = cfg.get("url"); - if (ocgUrl != null && !ocgUrl.toString().isBlank()) { - ocgEntry.put("url", ocgUrl.toString()); - } - Object ocgCredential = cfg.get("credential"); - if (ocgCredential != null && !ocgCredential.toString().isBlank()) { - // Same escaping path as HTTP/MCP headers: the secret name - // becomes a placeholder resolved at execution time, never - // a value baked into the workflow definition. - ocgEntry.put("auth", rewriteCredentialPlaceholders("Bearer ${" + ocgCredential + "}")); - } - ocgConfig.put(tool.getName(), ocgEntry); } } } @@ -447,7 +404,6 @@ public Object[] buildEnrichTask(String agentName, String llmRef, List ragConfig = new LinkedHashMap<>(); Map humanConfig = new LinkedHashMap<>(); Map wmqConfig = new LinkedHashMap<>(); - Map ocgConfig = new LinkedHashMap<>(); if (tools != null) { for (ToolConfig tool : tools) { @@ -1602,10 +1548,6 @@ public Object[] buildEnrichTaskDynamic( Map wmqEntry = new LinkedHashMap<>(); wmqEntry.put("batchSize", cfg.getOrDefault("batchSize", 1)); wmqConfig.put(tool.getName(), wmqEntry); - } else if (OCG_TOOL_TYPES.contains(toolType)) { - Map ocgEntry = new LinkedHashMap<>(); - ocgEntry.put("taskType", TYPE_MAP.getOrDefault(toolType, toolType.toUpperCase())); - ocgConfig.put(tool.getName(), ocgEntry); } // MCP config comes from runtime — skip here } @@ -1617,7 +1559,6 @@ public Object[] buildEnrichTaskDynamic( String ragJson = JavaScriptBuilder.toJson(ragConfig); String humanJson = JavaScriptBuilder.toJson(humanConfig); String wmqJson = JavaScriptBuilder.toJson(wmqConfig); - String ocgJson = JavaScriptBuilder.toJson(ocgConfig); Map knownToolNames = new LinkedHashMap<>(); if (tools != null) { for (ToolConfig t : tools) { @@ -1626,7 +1567,7 @@ public Object[] buildEnrichTaskDynamic( } String knownToolNamesJson = JavaScriptBuilder.toJson(knownToolNames); String script = JavaScriptBuilder.enrichToolsScriptDynamic( - httpJson, mediaJson, agentToolJson, ragJson, humanJson, wmqJson, ocgJson, knownToolNamesJson); + httpJson, mediaJson, agentToolJson, ragJson, humanJson, wmqJson, knownToolNamesJson); String enrichRef = agentName + "_" + p + "enrich_tools"; diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgCredentialResolver.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgCredentialResolver.java deleted file mode 100644 index 96488ea41..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgCredentialResolver.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -/** - * Resolves {@code #{NAME}} credential placeholders in a per-tool OCG auth - * header value (standalone mode). In embedded mode the host substitutes - * {@code ${workflow.secrets.NAME}} before the task ever starts, so values - * arrive with no placeholders and this resolver is never consulted. - * - *

    Mirrors the contract of {@code CredentialAwareHttpTask}: resolution is - * scoped to the calling user via the execution token in - * {@code __agentspan_ctx__}, and resolved values exist only in memory — - * they are never written back to the task model.

    - */ -@FunctionalInterface -public interface OcgCredentialResolver { - - /** - * Resolve every {@code #{NAME}} placeholder in {@code value}. - * - * @param value the auth header value, e.g. {@code "Bearer #{OCG_US_KEY}"} - * @param agentspanCtx the {@code __agentspan_ctx__} task input (map with an - * {@code execution_token} entry, or the raw token string) - * @return the fully resolved value, or {@code null} when resolution is not - * possible (missing/invalid token, unknown credential name) - */ - String resolve(String value, Object agentspanCtx); -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java deleted file mode 100644 index 90a318106..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgProperties.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -import lombok.Data; - -/** - * Configuration for the OCG (Open Context Graph) execution layer. - * - *

    OCG agents and tools are declared in user code via the SDK - * ({@code ocg_agent()} / {@code ocg_tools()}), and every OCG tool binds its - * own instance (url + credential-store reference) — there is no server-side - * OCG instance configuration. These properties only control whether the - * execution layer exists and how responses are shaped.

    - */ -@Data -@ConfigurationProperties(prefix = "agentspan.ocg") -public class OcgProperties { - - /** - * Whether the OCG execution layer (the {@code OCG_*} system tasks) is - * available. Disabling rejects agent starts that declare OCG tools. - * (Lombok generates {@code isEnabled()} for this field.) - */ - private boolean enabled = true; - - /** - * Per-response truncation cap (post-projection, JSON-serialized) for the - * {@code OCG_*} system tasks. Mirrors the Python reference helper - * {@code _enforce_response_cap}. - */ - private int responseCapChars = 8192; -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java deleted file mode 100644 index daaccf325..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRegisteredTaskDefs.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import java.util.List; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; - -import com.netflix.conductor.common.metadata.tasks.TaskDef; - -import dev.agentspan.runtime.ocg.operation.OcgCodeHistoryOperation; -import dev.agentspan.runtime.ocg.operation.OcgGetEntityOperation; -import dev.agentspan.runtime.ocg.operation.OcgMemoryDeleteOperation; -import dev.agentspan.runtime.ocg.operation.OcgMemoryReinforceOperation; -import dev.agentspan.runtime.ocg.operation.OcgMemorySetOperation; -import dev.agentspan.runtime.ocg.operation.OcgNeighborhoodOperation; -import dev.agentspan.runtime.ocg.operation.OcgQueryOperation; -import dev.agentspan.runtime.registry.RegisteredTaskDefs; - -/** - * OCG's contribution to the {@link RegisteredTaskDefs} registry. - * - *

    Conductor resolves dynamic-fork tasks by name; the seven - * {@code ocg_*} TaskDefs registered here let the OCG sub-agent's tool - * calls dispatch successfully at runtime. {@code retryCount = 0} on - * purpose — each call is a stateless HTTP round-trip handled by - * {@code OcgRequestTask} and the parent LLM loop owns retry decisions.

    - */ -@Component -@ConditionalOnProperty(prefix = "agentspan.ocg", name = "enabled", havingValue = "true", matchIfMissing = true) -public class OcgRegisteredTaskDefs implements RegisteredTaskDefs { - - private static final int OCG_TASK_TIMEOUT_SECONDS = 60; - private static final int OCG_TASK_RETRY_COUNT = 0; - private static final String OCG_TASK_OWNER_EMAIL = "ocg@agentspan.dev"; - - /** - * TaskDef names must equal {@code taskType.toLowerCase()} — that is the - * name the tool-dispatch script assigns to each scheduled OCG task, and - * Conductor resolves dynamic-fork tasks by name. The operation NAME - * constants ("query", "memory_set", …) are log/output labels, not task - * names — registering those leaves "ocg_query" unresolvable at dispatch. - */ - private static final List TASK_NAMES = List.of( - OcgQueryOperation.TASK_TYPE.toLowerCase(), - OcgGetEntityOperation.TASK_TYPE.toLowerCase(), - OcgNeighborhoodOperation.TASK_TYPE.toLowerCase(), - OcgCodeHistoryOperation.TASK_TYPE.toLowerCase(), - OcgMemorySetOperation.TASK_TYPE.toLowerCase(), - OcgMemoryReinforceOperation.TASK_TYPE.toLowerCase(), - OcgMemoryDeleteOperation.TASK_TYPE.toLowerCase()); - - @Override - public List taskDefs() { - return TASK_NAMES.stream().map(OcgRegisteredTaskDefs::buildTaskDef).toList(); - } - - private static TaskDef buildTaskDef(String name) { - TaskDef def = new TaskDef(); - def.setName(name); - def.setRetryCount(OCG_TASK_RETRY_COUNT); - def.setTimeoutSeconds(OCG_TASK_TIMEOUT_SECONDS); - def.setResponseTimeoutSeconds(OCG_TASK_TIMEOUT_SECONDS); - def.setOwnerEmail(OCG_TASK_OWNER_EMAIL); - return def; - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java deleted file mode 100644 index 52100db2c..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTask.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import java.io.IOException; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.regex.Pattern; - -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.netflix.conductor.core.execution.WorkflowExecutor; -import com.netflix.conductor.core.execution.tasks.WorkflowSystemTask; -import com.netflix.conductor.model.TaskModel; -import com.netflix.conductor.model.WorkflowModel; - -import dev.agentspan.runtime.ocg.operation.OcgInputs; -import dev.agentspan.runtime.ocg.operation.OcgOperation; -import dev.agentspan.runtime.ocg.operation.OcgTarget; - -/** - * System task that proxies a single OCG (Open Context Graph) operation. - * - *

    This class is the thin orchestrator: resolve target → send → project → - * cap → COMPLETED/FAILED. The endpoint-specific work (URL, method, body, - * field projection) lives in the strategy passed via {@link OcgOperation}. - * One {@link OcgRequestTask} bean per operation is registered by - * {@link OcgRequestTaskConfig}; the bean name is the operation's task - * type, which Conductor's {@code SystemTaskRegistry} dispatches on.

    - * - *

    Instance resolution, per call: a tool-bound instance arrives as - * the reserved {@code __ocg_url} / {@code __ocg_auth} task inputs (compiled - * from the SDK's {@code url=} / {@code credential=}) — there is no - * server-side default instance. {@code __ocg_auth} - * may carry a {@code #{NAME}} placeholder in standalone mode — resolved - * in-memory via {@link OcgCredentialResolver}, never written back to the - * task model. The reserved inputs are stripped before the operation sees - * the input map so they cannot leak into request bodies.

    - */ -public class OcgRequestTask extends WorkflowSystemTask { - - private static final Logger log = LoggerFactory.getLogger(OcgRequestTask.class); - - /** Reserved task-input key: per-tool OCG base URL. */ - public static final String INPUT_URL = "__ocg_url"; - - /** Reserved task-input key: per-tool Authorization header value. */ - public static final String INPUT_AUTH = "__ocg_auth"; - - private static final String INPUT_CTX = "__agentspan_ctx__"; - private static final Pattern PLACEHOLDER = Pattern.compile("#\\{[\\w.]+}"); - - private static final String TRUNCATE_MARKER = "...[truncated]"; - private static final int LOG_BODY_LIMIT = 256; - private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10); - - private final OcgOperation operation; - private final OcgProperties properties; - private final HttpClient httpClient; - private final OcgCredentialResolver credentialResolver; - - public OcgRequestTask(OcgOperation operation, OcgProperties properties, OcgCredentialResolver resolver) { - this(operation, properties, defaultHttpClient(), resolver); - } - - /** Visible-for-testing constructor with an injectable {@link HttpClient}. */ - OcgRequestTask(OcgOperation operation, OcgProperties properties, HttpClient httpClient) { - this(operation, properties, httpClient, null); - } - - /** - * Full constructor. {@code credentialResolver} may be null — per-tool - * {@code #{NAME}} auth placeholders then fail the task instead of - * leaking unresolved into the Authorization header. - */ - OcgRequestTask( - OcgOperation operation, - OcgProperties properties, - HttpClient httpClient, - OcgCredentialResolver credentialResolver) { - super(Objects.requireNonNull(operation, "operation").taskType()); - this.operation = operation; - this.properties = Objects.requireNonNull(properties, "properties"); - this.httpClient = Objects.requireNonNull(httpClient, "httpClient"); - this.credentialResolver = credentialResolver; - log.debug("OcgRequestTask registered (taskType={}, operation={})", operation.taskType(), operation.name()); - } - - @Override - public void start(WorkflowModel workflow, TaskModel task, WorkflowExecutor executor) { - Map rawInput = task.getInputData() != null ? task.getInputData() : Map.of(); - - String url = stringInput(rawInput.get(INPUT_URL)); - if (url == null) { - fail( - task, - "OCG " + operation.name() + " has no OCG instance bound: set url= on " - + "ocg_agent()/ocg_tools() in the SDK"); - return; - } - - String auth = stringInput(rawInput.get(INPUT_AUTH)); - if (auth != null) { - if (PLACEHOLDER.matcher(auth).find()) { - String resolved = - credentialResolver != null ? credentialResolver.resolve(auth, rawInput.get(INPUT_CTX)) : null; - if (resolved == null || PLACEHOLDER.matcher(resolved).find()) { - fail( - task, - "OCG " + operation.name() + " credential could not be resolved — check that the " - + "credential name exists in the credential store and the execution " - + "token is valid"); - return; - } - auth = resolved; - } - } - - OcgTarget target = new OcgTarget(url, auth); - try { - HttpResponse response = send(rawInput, target); - if (response.statusCode() < 200 || response.statusCode() >= 300) { - fail( - task, - "OCG " + operation.name() + " returned status " + response.statusCode() + ": " - + StringUtils.abbreviate(response.body(), LOG_BODY_LIMIT)); - return; - } - complete(task, response.body()); - } catch (InterruptedException e) { - // Re-flag the interrupt on the current thread so Conductor's - // executor (and anyone else up the stack) can observe the - // cancellation. Without this, a cancelled task would silently - // appear to "fail" without the interrupt ever propagating. - Thread.currentThread().interrupt(); - fail(task, "OCG " + operation.name() + " was interrupted"); - } catch (IOException | RuntimeException e) { - fail(task, "OCG " + operation.name() + " failed: " + e.getMessage()); - } - } - - private HttpResponse send(Map rawInput, OcgTarget target) - throws IOException, InterruptedException { - // Strip the instance-binding inputs so operations never see them — - // OcgMemorySetOperation forwards the whole map as the request body. - Map input = new LinkedHashMap<>(rawInput); - input.remove(INPUT_URL); - input.remove(INPUT_AUTH); - HttpRequest request = operation.build(target, input); - return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - } - - private static String stringInput(Object value) { - if (value == null) { - return null; - } - String s = value.toString(); - return s.isBlank() ? null : s; - } - - private void complete(TaskModel task, String body) throws IOException { - Object parsed = OcgInputs.parseJsonLenient(body); - Object projected = operation.project(parsed); - String serialized = OcgInputs.writeJson(projected); - // Apache StringUtils.abbreviate with a custom marker preserves the - // exact total-length contract callers depend on for context-window - // budgeting; max-width must be ≥ marker length to satisfy its API. - int cap = Math.max(TRUNCATE_MARKER.length(), properties.getResponseCapChars()); - String capped = StringUtils.abbreviate(serialized, TRUNCATE_MARKER, cap); - - Map output = new LinkedHashMap<>(); - output.put("result", capped); - output.put("operation", operation.name()); - task.setOutputData(output); - task.setStatus(TaskModel.Status.COMPLETED); - } - - private static void fail(TaskModel task, String reason) { - task.setOutputData(Map.of("error", reason)); - task.setReasonForIncompletion(reason); - task.setStatus(TaskModel.Status.FAILED); - } - - private static HttpClient defaultHttpClient() { - return HttpClient.newBuilder() - .connectTimeout(CONNECT_TIMEOUT) - .followRedirects(HttpClient.Redirect.NORMAL) - .build(); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java deleted file mode 100644 index b5a2edde4..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgRequestTaskConfig.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import dev.agentspan.runtime.credentials.CredentialResolutionService; -import dev.agentspan.runtime.credentials.ExecutionTokenService; -import dev.agentspan.runtime.ocg.operation.OcgCodeHistoryOperation; -import dev.agentspan.runtime.ocg.operation.OcgGetEntityOperation; -import dev.agentspan.runtime.ocg.operation.OcgMemoryDeleteOperation; -import dev.agentspan.runtime.ocg.operation.OcgMemoryReinforceOperation; -import dev.agentspan.runtime.ocg.operation.OcgMemorySetOperation; -import dev.agentspan.runtime.ocg.operation.OcgNeighborhoodOperation; -import dev.agentspan.runtime.ocg.operation.OcgQueryOperation; - -/** - * Registers one {@link OcgRequestTask} bean per OCG operation. Bean names - * match the Conductor task type strings so {@code SystemTaskRegistry} looks - * them up by type at dispatch time. - * - *

    Gated on {@code agentspan.ocg.enabled} (default {@code true}) rather - * than on a global URL: OCG instances are bound per-tool from the SDK - * ({@code url=} + {@code credential=}), so the tasks must exist even when - * no server-wide default instance is configured. A task dispatched with - * neither a per-tool URL nor {@code agentspan.ocg.url} fails with a - * pointer to both knobs.

    - */ -@Configuration -@EnableConfigurationProperties(OcgProperties.class) -@ConditionalOnProperty(prefix = "agentspan.ocg", name = "enabled", havingValue = "true", matchIfMissing = true) -public class OcgRequestTaskConfig { - - /** - * Placeholder resolution for per-tool {@code #{NAME}} auth values - * (standalone mode). When the credential services are absent (embedded - * hosts resolve {@code ${workflow.secrets.NAME}} themselves before the - * task starts), unresolved placeholders fail the task instead. - */ - @Bean - public OcgCredentialResolver ocgCredentialResolver( - ObjectProvider tokenService, - ObjectProvider resolutionService) { - ExecutionTokenService tokens = tokenService.getIfAvailable(); - CredentialResolutionService credentials = resolutionService.getIfAvailable(); - if (tokens == null || credentials == null) { - return (value, ctx) -> null; - } - return new PlaceholderCredentialResolver(tokens, credentials); - } - - @Bean(OcgQueryOperation.TASK_TYPE) - public OcgRequestTask ocgQueryTask(OcgProperties properties, OcgCredentialResolver resolver) { - return new OcgRequestTask(new OcgQueryOperation(), properties, resolver); - } - - @Bean(OcgGetEntityOperation.TASK_TYPE) - public OcgRequestTask ocgGetEntityTask(OcgProperties properties, OcgCredentialResolver resolver) { - return new OcgRequestTask(new OcgGetEntityOperation(), properties, resolver); - } - - @Bean(OcgNeighborhoodOperation.TASK_TYPE) - public OcgRequestTask ocgNeighborhoodTask(OcgProperties properties, OcgCredentialResolver resolver) { - return new OcgRequestTask(new OcgNeighborhoodOperation(), properties, resolver); - } - - @Bean(OcgCodeHistoryOperation.TASK_TYPE) - public OcgRequestTask ocgCodeHistoryTask(OcgProperties properties, OcgCredentialResolver resolver) { - return new OcgRequestTask(new OcgCodeHistoryOperation(), properties, resolver); - } - - @Bean(OcgMemorySetOperation.TASK_TYPE) - public OcgRequestTask ocgMemorySetTask(OcgProperties properties, OcgCredentialResolver resolver) { - return new OcgRequestTask(new OcgMemorySetOperation(), properties, resolver); - } - - @Bean(OcgMemoryReinforceOperation.TASK_TYPE) - public OcgRequestTask ocgMemoryReinforceTask(OcgProperties properties, OcgCredentialResolver resolver) { - return new OcgRequestTask(new OcgMemoryReinforceOperation(), properties, resolver); - } - - @Bean(OcgMemoryDeleteOperation.TASK_TYPE) - public OcgRequestTask ocgMemoryDeleteTask(OcgProperties properties, OcgCredentialResolver resolver) { - return new OcgRequestTask(new OcgMemoryDeleteOperation(), properties, resolver); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgToolValidator.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgToolValidator.java deleted file mode 100644 index 57221d2ae..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/OcgToolValidator.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import dev.agentspan.runtime.compiler.ToolCompiler; -import dev.agentspan.runtime.model.AgentConfig; -import dev.agentspan.runtime.model.ToolConfig; - -/** - * Start-time fail-fast for OCG tools: an OCG tool without a bound - * instance ({@code url} in its config) would otherwise surface as a task - * failure mid-conversation. Checked - * recursively — sub-agents and inline {@code agent_tool} children (which - * arrive as raw maps from the SDK serializer) carry OCG tools too. - */ -public final class OcgToolValidator { - - private OcgToolValidator() {} - - /** - * @param config the agent about to start (recursed into) - * @param properties the server's OCG properties, or {@code null} when the - * OCG feature is disabled ({@code agentspan.ocg.enabled=false}) - * @return an error message when the start request must be rejected - */ - public static Optional validate(AgentConfig config, OcgProperties properties) { - if (config == null) { - return Optional.empty(); - } - boolean featureEnabled = properties != null; - - Optional error = validateTools(config.getTools(), featureEnabled); - if (error.isPresent()) { - return error; - } - if (config.getAgents() != null) { - for (AgentConfig sub : config.getAgents()) { - error = validate(sub, properties); - if (error.isPresent()) { - return error; - } - } - } - return Optional.empty(); - } - - private static Optional validateTools(List tools, boolean featureEnabled) { - if (tools == null) { - return Optional.empty(); - } - for (ToolConfig tool : tools) { - String toolType = tool.getToolType(); - Map cfg = tool.getConfig(); - - if (toolType != null && ToolCompiler.OCG_TOOL_TYPES.contains(toolType)) { - Optional error = checkOcgTool(tool.getName(), cfg, featureEnabled); - if (error.isPresent()) { - return error; - } - } else if ("agent_tool".equals(toolType) && cfg != null) { - Optional error = validateChild(cfg.get("agentConfig"), featureEnabled); - if (error.isPresent()) { - return error; - } - } - } - return Optional.empty(); - } - - /** - * Inline {@code agent_tool} children arrive either as typed - * {@link AgentConfig} (built server-side) or as the raw map the SDK - * serializer produced — both shapes are walked. - */ - @SuppressWarnings("unchecked") - private static Optional validateChild(Object child, boolean featureEnabled) { - if (child instanceof AgentConfig typed) { - return validate(typed, featureEnabled ? new OcgProperties() : null); - } - if (!(child instanceof Map childMap)) { - return Optional.empty(); - } - - Object tools = childMap.get("tools"); - if (tools instanceof List toolList) { - for (Object t : toolList) { - if (!(t instanceof Map toolMap)) { - continue; - } - String toolType = asString(toolMap.get("toolType")); - Map cfg = toolMap.get("config") instanceof Map m ? (Map) m : null; - if (toolType != null && ToolCompiler.OCG_TOOL_TYPES.contains(toolType)) { - Optional error = checkOcgTool(asString(toolMap.get("name")), cfg, featureEnabled); - if (error.isPresent()) { - return error; - } - } else if ("agent_tool".equals(toolType) && cfg != null) { - Optional error = validateChild(cfg.get("agentConfig"), featureEnabled); - if (error.isPresent()) { - return error; - } - } - } - } - Object agents = childMap.get("agents"); - if (agents instanceof List agentList) { - for (Object sub : agentList) { - Optional error = validateChild(sub, featureEnabled); - if (error.isPresent()) { - return error; - } - } - } - return Optional.empty(); - } - - private static Optional checkOcgTool(String name, Map cfg, boolean featureEnabled) { - String toolName = name != null ? name : "(unnamed)"; - if (!featureEnabled) { - return Optional.of("OCG tool '" + toolName + "' cannot run: OCG is disabled on this server " - + "(agentspan.ocg.enabled=false). Remove the OCG tools or enable OCG."); - } - String url = cfg != null ? asString(cfg.get("url")) : null; - if (url == null || url.isBlank()) { - return Optional.of("OCG tool '" + toolName + "' has no OCG instance bound: set url= on " - + "ocg_agent()/ocg_tools() in the SDK."); - } - return Optional.empty(); - } - - private static String asString(Object value) { - return value != null ? value.toString() : null; - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/PlaceholderCredentialResolver.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/PlaceholderCredentialResolver.java deleted file mode 100644 index d8ce3181d..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/PlaceholderCredentialResolver.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg; - -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import dev.agentspan.runtime.credentials.CredentialResolutionService; -import dev.agentspan.runtime.credentials.ExecutionTokenService; - -/** - * Default {@link OcgCredentialResolver}: resolves {@code #{NAME}} placeholders - * through the credential store, scoped to the user identified by the execution - * token in {@code __agentspan_ctx__}. Same token/store contract as - * {@code CredentialAwareHttpTask} — OCG must not invent a second secret path. - */ -public class PlaceholderCredentialResolver implements OcgCredentialResolver { - - private static final Logger log = LoggerFactory.getLogger(PlaceholderCredentialResolver.class); - private static final Pattern PLACEHOLDER = Pattern.compile("#\\{([\\w.]+)}"); - - private final ExecutionTokenService tokenService; - private final CredentialResolutionService resolutionService; - - public PlaceholderCredentialResolver( - ExecutionTokenService tokenService, CredentialResolutionService resolutionService) { - this.tokenService = tokenService; - this.resolutionService = resolutionService; - } - - @Override - public String resolve(String value, Object agentspanCtx) { - if (value == null || !PLACEHOLDER.matcher(value).find()) { - return value; - } - String userId = extractUserId(agentspanCtx); - if (userId == null) { - return null; - } - Matcher m = PLACEHOLDER.matcher(value); - StringBuilder sb = new StringBuilder(); - while (m.find()) { - String credValue = resolutionService.resolve(userId, m.group(1)); - if (credValue == null) { - log.warn("OCG credential '{}' not found for user", m.group(1)); - return null; - } - m.appendReplacement(sb, Matcher.quoteReplacement(credValue)); - } - m.appendTail(sb); - return sb.toString(); - } - - private String extractUserId(Object ctx) { - String token = null; - if (ctx instanceof Map ctxMap) { - token = (String) ctxMap.get("execution_token"); - } else if (ctx instanceof String s) { - token = s; - } - if (token == null) { - return null; - } - try { - return tokenService.validate(token).userId(); - } catch (Exception e) { - log.warn("Failed to validate token for OCG credential resolution: {}", e.getMessage()); - return null; - } - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java deleted file mode 100644 index e8459835d..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgCodeHistoryOperation.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg.operation; - -import java.net.URI; -import java.net.http.HttpRequest; -import java.util.Map; - -/** {@code GET /api/v1/code/history/{repo_id}?path=...&limit=N} — file commit history. */ -public final class OcgCodeHistoryOperation implements OcgOperation { - - public static final String TASK_TYPE = "OCG_CODE_HISTORY"; - public static final String NAME = "code_history"; - - private static final int DEFAULT_LIMIT = 20; - - @Override - public String taskType() { - return TASK_TYPE; - } - - @Override - public String name() { - return NAME; - } - - @Override - public HttpRequest build(OcgTarget target, Map input) { - String repoId = OcgInputs.required(input, "repo_id"); - String path = OcgInputs.required(input, "path"); - URI uri = OcgUri.forApi(target) - .pathSegment("code", "history", repoId) - .queryParam("path", path) - .queryParam("limit", OcgInputs.intOrDefault(input.get("limit"), DEFAULT_LIMIT)) - .build() - .toUri(); - return OcgRequest.get(target, uri); - } - - @Override - public Object project(Object raw) { - if (!(raw instanceof Map map)) return raw; - return OcgInputs.pick(map, "commits", "repo_id", "path"); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java deleted file mode 100644 index a3bba85d5..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgGetEntityOperation.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg.operation; - -import java.net.URI; -import java.net.http.HttpRequest; -import java.util.Map; - -/** {@code GET /api/v1/entities/{entity_id}} — single entity lookup by id. */ -public final class OcgGetEntityOperation implements OcgOperation { - - public static final String TASK_TYPE = "OCG_GET_ENTITY"; - public static final String NAME = "get_entity"; - - @Override - public String taskType() { - return TASK_TYPE; - } - - @Override - public String name() { - return NAME; - } - - @Override - public HttpRequest build(OcgTarget target, Map input) { - String entityId = OcgInputs.required(input, "entity_id"); - URI uri = - OcgUri.forApi(target).pathSegment("entities", entityId).build().toUri(); - return OcgRequest.get(target, uri); - } - - @Override - public Object project(Object raw) { - if (!(raw instanceof Map map)) return raw; - return OcgInputs.pick(map, "id", "type", "title", "properties"); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java deleted file mode 100644 index 0a1f48584..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgInputs.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg.operation; - -import java.util.LinkedHashMap; -import java.util.Map; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; -import org.apache.commons.lang3.math.NumberUtils; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Map and JSON utilities shared by every {@link OcgOperation} and by - * {@code OcgRequestTask}. Stateless; Apache Commons wherever it gives a - * cleaner one-liner than rolling our own. - * - *

    Owns the single {@link ObjectMapper} used across the OCG subsystem so - * there's one source of truth for JSON shape and no per-class instances.

    - */ -public final class OcgInputs { - - /** Shared Jackson mapper. {@link ObjectMapper} is thread-safe. */ - public static final ObjectMapper MAPPER = new ObjectMapper(); - - private OcgInputs() {} - - /** - * Build a new {@link LinkedHashMap} containing only the requested keys - * that are present and non-null in {@code src}. Insertion order is the - * key order passed in — matters for stable JSON serialization of - * request bodies. - */ - public static Map pick(Map src, String... keys) { - Map out = new LinkedHashMap<>(); - for (String key : keys) { - Object value = src.get(key); - if (value != null) { - out.put(key, value); - } - } - return out; - } - - /** - * Extract a required string input. Throws {@link IllegalArgumentException} - * (via {@link Validate}) with a consistent message when missing or blank, - * giving the OCG sub-agent's LLM a debuggable failure reason instead of - * an opaque NPE downstream. - */ - public static String required(Map input, String key) { - Object value = input.get(key); - Validate.isTrue( - value instanceof String && StringUtils.isNotBlank((String) value), - "Missing or empty required input '%s'", - key); - return (String) value; - } - - /** - * Coerce {@code value} to int, falling back when it's null, non-numeric, - * or an unparseable string. Number → intValue(), String → parsed via - * {@link NumberUtils#toInt(String, int)} so we don't throw on bad input. - */ - public static int intOrDefault(Object value, int fallback) { - if (value instanceof Number n) return n.intValue(); - if (value instanceof String s) return NumberUtils.toInt(s, fallback); - return fallback; - } - - /** - * Parse a JSON body without throwing on malformed input. Blank → empty - * map; parse failure → {@code {"raw": }} so the original text is - * still visible downstream rather than swallowed. - */ - public static Object parseJsonLenient(String body) { - if (StringUtils.isBlank(body)) return Map.of(); - try { - return MAPPER.readValue(body, Object.class); - } catch (Exception e) { - return Map.of("raw", body); - } - } - - /** Serialize an object to JSON using the shared mapper. */ - public static String writeJson(Object value) throws JsonProcessingException { - return MAPPER.writeValueAsString(value); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java deleted file mode 100644 index e4175a505..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryDeleteOperation.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg.operation; - -import java.net.http.HttpRequest; -import java.util.Map; - -import org.apache.commons.lang3.StringUtils; -import org.springframework.web.util.UriComponentsBuilder; - -/** {@code DELETE /api/v1/memories/{key}?agent=...&user=...} — remove a memory by key. */ -public final class OcgMemoryDeleteOperation implements OcgOperation { - - public static final String TASK_TYPE = "OCG_MEMORY_DELETE"; - public static final String NAME = "memory_delete"; - - @Override - public String taskType() { - return TASK_TYPE; - } - - @Override - public String name() { - return NAME; - } - - @Override - public HttpRequest build(OcgTarget target, Map input) { - String key = OcgInputs.required(input, "key"); - UriComponentsBuilder uri = OcgUri.forApi(target).pathSegment("memories", key); - addQueryParamIfPresent(uri, "agent", input.get("agent")); - addQueryParamIfPresent(uri, "user", input.get("user")); - return OcgRequest.delete(target, uri.build().toUri()); - } - - private static void addQueryParamIfPresent(UriComponentsBuilder uri, String name, Object value) { - if (value == null) return; - String s = value.toString(); - if (StringUtils.isNotEmpty(s)) { - uri.queryParam(name, s); - } - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java deleted file mode 100644 index f52051453..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemoryReinforceOperation.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg.operation; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpRequest; -import java.util.Map; - -/** {@code POST /api/v1/memories/{key}/reinforce} — confidence boost on re-observation. */ -public final class OcgMemoryReinforceOperation implements OcgOperation { - - public static final String TASK_TYPE = "OCG_MEMORY_REINFORCE"; - public static final String NAME = "memory_reinforce"; - - @Override - public String taskType() { - return TASK_TYPE; - } - - @Override - public String name() { - return NAME; - } - - @Override - public HttpRequest build(OcgTarget target, Map input) throws IOException { - String key = OcgInputs.required(input, "key"); - Map body = OcgInputs.pick(input, "agent", "user", "confidence_boost", "source_ref"); - URI uri = OcgUri.forApi(target) - .pathSegment("memories", key, "reinforce") - .build() - .toUri(); - return OcgRequest.postJson(target, uri, body); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java deleted file mode 100644 index f40a1b25d..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgMemorySetOperation.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg.operation; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpRequest; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * {@code POST /api/v1/memories} — create or overwrite a memory. - * - *

    Body is the input map verbatim minus the {@code __agentspan_ctx__} - * execution-token glob, which is server-side plumbing and should never be - * forwarded to OCG. Identity projection — OCG's memory response is small - * enough to surface unchanged.

    - */ -public final class OcgMemorySetOperation implements OcgOperation { - - public static final String TASK_TYPE = "OCG_MEMORY_SET"; - public static final String NAME = "memory_set"; - - @Override - public String taskType() { - return TASK_TYPE; - } - - @Override - public String name() { - return NAME; - } - - @Override - public HttpRequest build(OcgTarget target, Map input) throws IOException { - Map body = new LinkedHashMap<>(input); - body.remove("__agentspan_ctx__"); - URI uri = OcgUri.forApi(target).pathSegment("memories").build().toUri(); - return OcgRequest.postJson(target, uri, body); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java deleted file mode 100644 index 626a8dfaf..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgNeighborhoodOperation.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg.operation; - -import java.net.URI; -import java.net.http.HttpRequest; -import java.util.Map; - -/** {@code GET /api/v1/graph/neighborhood/{entity_id}?depth=N&limit=M} — graph traversal. */ -public final class OcgNeighborhoodOperation implements OcgOperation { - - public static final String TASK_TYPE = "OCG_NEIGHBORHOOD"; - public static final String NAME = "neighborhood"; - - private static final int DEFAULT_DEPTH = 2; - private static final int DEFAULT_LIMIT = 50; - - @Override - public String taskType() { - return TASK_TYPE; - } - - @Override - public String name() { - return NAME; - } - - @Override - public HttpRequest build(OcgTarget target, Map input) { - String entityId = OcgInputs.required(input, "entity_id"); - URI uri = OcgUri.forApi(target) - .pathSegment("graph", "neighborhood", entityId) - .queryParam("depth", OcgInputs.intOrDefault(input.get("depth"), DEFAULT_DEPTH)) - .queryParam("limit", OcgInputs.intOrDefault(input.get("limit"), DEFAULT_LIMIT)) - .build() - .toUri(); - return OcgRequest.get(target, uri); - } - - @Override - public Object project(Object raw) { - if (!(raw instanceof Map map)) return raw; - return OcgInputs.pick(map, "center", "edges", "neighbors"); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java deleted file mode 100644 index ad8984d64..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgOperation.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg.operation; - -import java.io.IOException; -import java.net.http.HttpRequest; -import java.util.Map; - -/** - * Strategy for a single OCG endpoint. One implementation per {@code OCG_*} - * task type; each owns the URL/method/body for its endpoint plus the - * field-projection rule for shrinking the raw response before it reaches - * the LLM. - * - *

    Implementations are stateless and reused across calls — the per-call - * inputs flow in via {@link #build}, never via constructors.

    - */ -public interface OcgOperation { - - /** Conductor task type string this operation registers under (e.g. {@code "OCG_QUERY"}). */ - String taskType(); - - /** Short operation name used in logs and the task output's {@code operation} field. */ - String name(); - - /** - * Build the HTTP request for this operation. Implementations should - * use {@link OcgRequest} and {@link OcgUri} so authentication headers - * and base-URL handling stay consistent across endpoints. - * - *

    {@link IOException} (which includes Jackson's - * {@code JsonProcessingException}) is the only checked exception - * implementations may throw — request building is purely an I/O-shape - * concern and should not surface arbitrary checked exceptions.

    - */ - HttpRequest build(OcgTarget target, Map input) throws IOException; - - /** - * Project the parsed JSON response down to the fields the LLM needs. - * Default is identity — only implementations that strip noise (e.g. - * scoring metadata, internal ids) need to override. - */ - default Object project(Object rawResponse) { - return rawResponse; - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java deleted file mode 100644 index 2f41cca13..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgQueryOperation.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg.operation; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpRequest; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** {@code POST /api/v1/agent/query} — natural-language retrieval over the graph. */ -public final class OcgQueryOperation implements OcgOperation { - - public static final String TASK_TYPE = "OCG_QUERY"; - public static final String NAME = "query"; - - @Override - public String taskType() { - return TASK_TYPE; - } - - @Override - public String name() { - return NAME; - } - - @Override - public HttpRequest build(OcgTarget target, Map input) throws IOException { - Map body = - OcgInputs.pick(input, "query", "max_results", "traversal_level", "start_time", "end_time"); - URI uri = OcgUri.forApi(target).pathSegment("agent", "query").build().toUri(); - return OcgRequest.postJson(target, uri, body); - } - - /** - * Keep citations (the only piece the LLM acts on) and traversal_results - * when present; drop scoring metadata, embedding vectors, and any other - * fields the OCG service may add over time. - */ - @Override - public Object project(Object raw) { - if (!(raw instanceof Map map)) return raw; - Map out = new LinkedHashMap<>(); - out.put("citations", projectCitations(map.get("citations"))); - if (map.containsKey("traversal_results")) { - out.put("traversal_results", map.get("traversal_results")); - } - return out; - } - - private static List> projectCitations(Object raw) { - if (!(raw instanceof List list)) return List.of(); - List> out = new ArrayList<>(); - for (Object item : list) { - if (item instanceof Map citation) { - out.add(OcgInputs.pick(citation, "source_item_id", "title", "container_id", "snippet")); - } - } - return out; - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java deleted file mode 100644 index b5f6afa15..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgRequest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg.operation; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpRequest; -import java.net.http.HttpRequest.BodyPublishers; -import java.nio.charset.StandardCharsets; -import java.time.Duration; - -/** - * HTTP request factory for OCG endpoints. Centralises the bearer-auth - * header, default timeouts, and JSON body serialization so every - * {@link OcgOperation} produces requests with the same baseline headers - * and policies — drift here would mean some endpoints get authenticated - * and others silently don't. - */ -public final class OcgRequest { - - private static final Duration READ_TIMEOUT = Duration.ofSeconds(30); - - private OcgRequest() {} - - /** - * Common {@link HttpRequest.Builder} for every OCG endpoint: read - * timeout, JSON accept, and the optional bearer token. Operations call - * {@code .uri(...).METHOD(...).build()} on the returned builder. - */ - public static HttpRequest.Builder base(OcgTarget target) { - HttpRequest.Builder b = HttpRequest.newBuilder().timeout(READ_TIMEOUT).header("Accept", "application/json"); - if (target.hasAuth()) { - b.header("Authorization", target.authHeader()); - } - return b; - } - - /** POST with a JSON-serialized body. */ - public static HttpRequest postJson(OcgTarget target, URI uri, Object body) throws IOException { - String json = OcgInputs.writeJson(body); - return base(target) - .uri(uri) - .header("Content-Type", "application/json") - .POST(BodyPublishers.ofString(json, StandardCharsets.UTF_8)) - .build(); - } - - /** GET with no body. */ - public static HttpRequest get(OcgTarget target, URI uri) { - return base(target).uri(uri).GET().build(); - } - - /** DELETE with no body. */ - public static HttpRequest delete(OcgTarget target, URI uri) { - return base(target).uri(uri).DELETE().build(); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgTarget.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgTarget.java deleted file mode 100644 index e093ec7b3..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgTarget.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg.operation; - -/** - * The OCG instance a single call targets: base URL plus the full - * {@code Authorization} header value (already resolved — no credential - * placeholders survive past {@code OcgRequestTask}). - * - *

    Resolved per call by {@code OcgRequestTask}: a tool-bound instance - * (the {@code __ocg_url} / {@code __ocg_auth} task inputs compiled from the - * SDK's {@code url=} / {@code credential=}) wins over the server-wide - * default in {@code OcgProperties}. Operations only ever see this record, - * so they cannot accidentally reach for the default config.

    - * - * @param baseUrl base URL of the OCG instance (no trailing slash required) - * @param authHeader full Authorization header value (e.g. {@code "Bearer …"}), - * or {@code null} / blank for unauthenticated instances - */ -public record OcgTarget(String baseUrl, String authHeader) { - - public boolean hasAuth() { - return authHeader != null && !authHeader.isBlank(); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java deleted file mode 100644 index 7db7942de..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ocg/operation/OcgUri.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.ocg.operation; - -import org.apache.commons.lang3.StringUtils; -import org.springframework.web.util.UriComponentsBuilder; - -/** - * URI builder for OCG endpoints. Every OCG path sits under {@code /api/v1} - * on the configured base URL; this helper handles the trailing-slash - * trimming and the prefix attachment so operations only spell out the - * endpoint-specific path segments. - * - *

    Path segments and query params go through {@link UriComponentsBuilder}, - * which URL-encodes correctly — no hand-rolled {@code URLEncoder} calls - * scattered across operations.

    - */ -public final class OcgUri { - - /** OCG's stable API version prefix. Bump as a single point of change. */ - public static final String API_PREFIX_V1 = "/api/v1"; - - private OcgUri() {} - - /** - * Returns a {@link UriComponentsBuilder} rooted at - * {@code /api/v1}. Operations chain {@code .pathSegment(...)} - * + {@code .queryParam(...)} on top. - */ - public static UriComponentsBuilder forApi(OcgTarget target) { - String base = StringUtils.removeEnd(StringUtils.defaultString(target.baseUrl()), "/"); - return UriComponentsBuilder.fromUriString(base + API_PREFIX_V1); - } -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java index 6b0694038..eafcc8746 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java @@ -14,7 +14,7 @@ * entries to the metadata store at startup. * *

    Conductor's dynamic-fork dispatcher resolves tasks by name - * via the TaskDef registry. Custom system task types (e.g. {@code OCG_QUERY}) + * via the TaskDef registry. Custom system task types * therefore need a matching TaskDef registered before any workflow can * dispatch them. Beans of this type plug into * {@link RegisteredTaskDefsRegistrar} and the registration happens diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java index b17d0f6ae..8e82e3440 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/service/AgentService.java @@ -48,8 +48,6 @@ import dev.agentspan.runtime.credentials.ExecutionTokenService; import dev.agentspan.runtime.model.*; import dev.agentspan.runtime.normalizer.NormalizerRegistry; -import dev.agentspan.runtime.ocg.OcgProperties; -import dev.agentspan.runtime.ocg.OcgToolValidator; import dev.agentspan.runtime.util.ModelParser; import dev.agentspan.runtime.util.ProviderValidator; @@ -98,14 +96,6 @@ public class AgentService { @Autowired(required = false) private MetadataService metadataService; - /** - * OCG execution-layer config. Null when {@code agentspan.ocg.enabled=false} - * (the bean only exists while {@code OcgRequestTaskConfig} is active), which - * {@link OcgToolValidator} treats as "OCG tools are unavailable". - */ - @Autowired(required = false) - private OcgProperties ocgProperties; - /** Package-private constructor for testing with ExecutionTokenService */ AgentService( AgentCompiler agentCompiler, @@ -266,12 +256,6 @@ public StartResponse start(StartRequest request) { validateStartInput(request); AgentConfig config = resolveConfig(request); - // Fail fast on OCG tools with no instance to run against — otherwise - // this surfaces as a task failure mid-conversation. - OcgToolValidator.validate(config, ocgProperties).ifPresent(err -> { - throw new IllegalArgumentException(err); - }); - // Apply per-call timeout override from StartRequest if (request.getTimeoutSeconds() != null && request.getTimeoutSeconds() > 0) { config.setTimeoutSeconds(request.getTimeoutSeconds()); diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java index 79a416338..91557657c 100644 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java +++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java @@ -526,7 +526,6 @@ public static String enrichToolsScript( String cliConfigJson, String humanConfigJson, String wmqConfigJson, - String ocgConfigJson, String knownToolNamesJson) { return iife(" var httpCfg = " + httpConfigJson + ";" + " var mcpCfg = " + mcpConfigJson + ";" + " var mediaCfg = " @@ -535,8 +534,7 @@ public static String enrichToolsScript( + ragConfigJson + ";" + " var cliCfg = " + cliConfigJson + ";" + " var humanCfg = " + humanConfigJson + ";" + " var wmqCfg = " - + wmqConfigJson + ";" + " var ocgCfg = " - + ocgConfigJson + ";" + " var knownNames = " + knownToolNamesJson + ";" + + wmqConfigJson + ";" + " var knownNames = " + knownToolNamesJson + ";" + " var agentState = $.agentState || {};" + " var tcs = $.toolCalls || [];" + " var result = [];" @@ -548,7 +546,7 @@ public static String enrichToolsScript( // SIMPLE task gets queued under the unknown name with no worker // polling for it and the workflow hangs forever. + " var isCfg = !!(httpCfg[n] || mcpCfg[n] || agentToolCfg[n] ||" - + " mediaCfg[n] || ragCfg[n] || humanCfg[n] || wmqCfg[n] || ocgCfg[n]);" + + " mediaCfg[n] || ragCfg[n] || humanCfg[n] || wmqCfg[n]);" // Reject any name not in the agent's declared tools. The // previous gate (``hasKnownNames``) skipped this check when // ``knownNames`` was empty, which allowed an agent declared @@ -578,13 +576,41 @@ public static String enrichToolsScript( + " retryDelaySeconds: 2};" + " if (httpCfg[n]) {" + " t.type = 'HTTP';" + + " var hc = httpCfg[n];" + + " var hargs = tc.inputParameters || {};" + + " var huri = hc.url || '';" + + " var hbody = hargs;" + // Optional URI templating: pathTemplate consumes {param}s from + // the LLM's arguments (URL-encoded); queryParams append the + // listed args as a query string. Consumed args leave the body. + // Tools without these keys keep the static-uri/args-as-body + // shape unchanged. + + " if (hc.pathTemplate || hc.queryParams) {" + + " var consumed = {};" + + " if (hc.pathTemplate) {" + + " huri = huri + hc.pathTemplate.replace(/\\{(\\w+)\\}/g," + + " function(m, k) { consumed[k] = true;" + + " return encodeURIComponent(String(hargs[k] == null ? '' : hargs[k])); });" + + " }" + + " if (hc.queryParams) {" + + " var qs = [];" + + " for (var qi = 0; qi < hc.queryParams.length; qi++) {" + + " var qk = hc.queryParams[qi];" + + " if (hargs[qk] != null && hargs[qk] !== '') { consumed[qk] = true;" + + " qs.push(encodeURIComponent(qk) + '=' + encodeURIComponent(String(hargs[qk]))); }" + + " }" + + " if (qs.length) { huri = huri + (huri.indexOf('?') >= 0 ? '&' : '?') + qs.join('&'); }" + + " }" + + " hbody = {};" + + " for (var hk in hargs) { if (!consumed[hk]) hbody[hk] = hargs[hk]; }" + + " }" + " t.inputParameters = {http_request: {" - + " uri: httpCfg[n].url || ''," - + " method: httpCfg[n].method || 'GET'," - + " headers: httpCfg[n].headers || {}," - + " body: tc.inputParameters || {}," - + " accept: httpCfg[n].accept || 'application/json'," - + " contentType: httpCfg[n].contentType || 'application/json'," + + " uri: huri," + + " method: hc.method || 'GET'," + + " headers: hc.headers || {}," + + " body: hbody," + + " accept: hc.accept || 'application/json'," + + " contentType: hc.contentType || 'application/json'," + " connectionTimeOut: 30000," + " readTimeOut: 30000}};" + " if ($.agentspanCtx) { t.inputParameters.__agentspan_ctx__ = $.agentspanCtx; }" @@ -642,18 +668,6 @@ public static String enrichToolsScript( + " var inp = tc.inputParameters || {};" + " for (var k in inp) { merged[k] = inp[k]; }" + " t.inputParameters = merged;" - + " } else if (ocgCfg[n]) {" - // OCG tools dispatch to per-operation OCG_* system tasks. The - // LLM-supplied arguments are forwarded verbatim; a per-tool - // instance binding (url + auth placeholder) rides along as - // reserved __ocg_* inputs. Tools without one fall back to the - // server default (OcgProperties) inside OcgRequestTask. - + " t.type = ocgCfg[n].taskType;" - + " t.name = ocgCfg[n].taskType.toLowerCase();" - + " t.inputParameters = tc.inputParameters || {};" - + " if (ocgCfg[n].url) { t.inputParameters.__ocg_url = ocgCfg[n].url; }" - + " if (ocgCfg[n].auth) { t.inputParameters.__ocg_auth = ocgCfg[n].auth; }" - + " if ($.agentspanCtx) { t.inputParameters.__agentspan_ctx__ = $.agentspanCtx; }" + " } else if (humanCfg[n]) {" + " t.type = 'HUMAN';" + " t.name = n;" @@ -1138,7 +1152,6 @@ public static String enrichToolsScriptDynamic( String ragConfigJson, String humanConfigJson, String wmqConfigJson, - String ocgConfigJson, String knownToolNamesJson) { return iife(" var httpCfg = " + httpConfigJson + ";" + " var mcpCfg = $.mcpConfig || {};" + " var apiCfg = $.apiConfig || {};" @@ -1147,8 +1160,7 @@ public static String enrichToolsScriptDynamic( + agentToolConfigJson + ";" + " var ragCfg = " + ragConfigJson + ";" + " var humanCfg = " + humanConfigJson + ";" + " var wmqCfg = " - + wmqConfigJson + ";" + " var ocgCfg = " - + ocgConfigJson + ";" + " var knownNames = " + knownToolNamesJson + ";" + + wmqConfigJson + ";" + " var knownNames = " + knownToolNamesJson + ";" + " var agentState = $.agentState || {};" + " var tcs = $.toolCalls || [];" + " var result = [];" @@ -1158,7 +1170,7 @@ public static String enrichToolsScriptDynamic( // for context). Without this the SIMPLE task gets queued under // an unknown name and the workflow hangs forever. + " var isCfg = !!(httpCfg[n] || mcpCfg[n] || apiCfg[n] || agentToolCfg[n] ||" - + " mediaCfg[n] || ragCfg[n] || humanCfg[n] || wmqCfg[n] || ocgCfg[n]);" + + " mediaCfg[n] || ragCfg[n] || humanCfg[n] || wmqCfg[n]);" // See ``enrichToolsScript`` above — empty knownNames means // NO tool is callable by the LLM (locks down the prefill-only // leak path). @@ -1183,13 +1195,41 @@ public static String enrichToolsScriptDynamic( + " retryDelaySeconds: 2};" + " if (httpCfg[n]) {" + " t.type = 'HTTP';" + + " var hc = httpCfg[n];" + + " var hargs = tc.inputParameters || {};" + + " var huri = hc.url || '';" + + " var hbody = hargs;" + // Optional URI templating: pathTemplate consumes {param}s from + // the LLM's arguments (URL-encoded); queryParams append the + // listed args as a query string. Consumed args leave the body. + // Tools without these keys keep the static-uri/args-as-body + // shape unchanged. + + " if (hc.pathTemplate || hc.queryParams) {" + + " var consumed = {};" + + " if (hc.pathTemplate) {" + + " huri = huri + hc.pathTemplate.replace(/\\{(\\w+)\\}/g," + + " function(m, k) { consumed[k] = true;" + + " return encodeURIComponent(String(hargs[k] == null ? '' : hargs[k])); });" + + " }" + + " if (hc.queryParams) {" + + " var qs = [];" + + " for (var qi = 0; qi < hc.queryParams.length; qi++) {" + + " var qk = hc.queryParams[qi];" + + " if (hargs[qk] != null && hargs[qk] !== '') { consumed[qk] = true;" + + " qs.push(encodeURIComponent(qk) + '=' + encodeURIComponent(String(hargs[qk]))); }" + + " }" + + " if (qs.length) { huri = huri + (huri.indexOf('?') >= 0 ? '&' : '?') + qs.join('&'); }" + + " }" + + " hbody = {};" + + " for (var hk in hargs) { if (!consumed[hk]) hbody[hk] = hargs[hk]; }" + + " }" + " t.inputParameters = {http_request: {" - + " uri: httpCfg[n].url || ''," - + " method: httpCfg[n].method || 'GET'," - + " headers: httpCfg[n].headers || {}," - + " body: tc.inputParameters || {}," - + " accept: httpCfg[n].accept || 'application/json'," - + " contentType: httpCfg[n].contentType || 'application/json'," + + " uri: huri," + + " method: hc.method || 'GET'," + + " headers: hc.headers || {}," + + " body: hbody," + + " accept: hc.accept || 'application/json'," + + " contentType: hc.contentType || 'application/json'," + " connectionTimeOut: 30000," + " readTimeOut: 30000}};" + " if ($.agentspanCtx) { t.inputParameters.__agentspan_ctx__ = $.agentspanCtx; }" @@ -1280,13 +1320,6 @@ public static String enrichToolsScriptDynamic( + " var inp = tc.inputParameters || {};" + " for (var k in inp) { merged[k] = inp[k]; }" + " t.inputParameters = merged;" - + " } else if (ocgCfg[n]) {" - + " t.type = ocgCfg[n].taskType;" - + " t.name = ocgCfg[n].taskType.toLowerCase();" - + " t.inputParameters = tc.inputParameters || {};" - + " if (ocgCfg[n].url) { t.inputParameters.__ocg_url = ocgCfg[n].url; }" - + " if (ocgCfg[n].auth) { t.inputParameters.__ocg_auth = ocgCfg[n].auth; }" - + " if ($.agentspanCtx) { t.inputParameters.__agentspan_ctx__ = $.agentspanCtx; }" + " } else if (humanCfg[n]) {" + " t.type = 'HUMAN';" + " t.name = n;" From 2ac990cb9a0f5565e73d66e55d1ee1dd92716e14 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Fri, 12 Jun 2026 13:49:20 -0700 Subject: [PATCH 30/61] OCG live-tuning from dev runs; drop ocg_code_history; rewrite flow doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardening from live runs against dev OCG on the embedded server: - start_time/end_time schemas + prompt now require full RFC3339 — bare dates got 400s from the OCG API and pointless Conductor retries. - max_results carries a schema-level maximum of 100 (default 10); traversal_level defaults to 0. With responses injected verbatim into the calling LLM's context, an uncapped max_results produced a 162KB response that destroyed the retriever's context. - Canned retrieval prompt gains a retrieval budget: at most 3 distinct keyword queries, no rephrasing — OCG is embedding search, not a conversation partner. - Example main agents (116/117/smoke) get explicit one-retrieval instructions and small max_turns; positive phrasing (telling gpt-4o-mini to "STOP" made it emit refusal-style non-answers). ocg_code_history removed from the toolset (and its subset switch); six tools remain. docs/ocg-agent-flow.md fully rewritten for the first release: both usage shapes with mermaid sequence diagrams, an execution-flow diagram (SDK config → compiler → enrich templating → HTTP task → secrets store → OCG), the tools table, and LLM-discipline notes. No migration content. Verified live: both examples green on the embedded orkes server — sub-agent shape 1 round / 3 distinct queries / ~6KB responses / 10.2k tokens; direct shape 4 queries / ~3KB responses / 5.3k tokens. Co-Authored-By: Claude Fable 5 --- .../2026-06-12-ocg-sdk-subagent-status.md | 17 + docs/ocg-agent-flow.md | 342 +++++++++++------- docs/python-sdk/api-reference.md | 2 +- e2e/ocg/jira_ocg_smoke.py | 7 +- sdk/python/examples/116_ocg_subagent.py | 19 +- sdk/python/examples/117_ocg_direct_tools.py | 20 +- sdk/python/src/agentspan/agents/ocg.py | 76 ++-- sdk/python/tests/unit/test_ocg.py | 20 +- 8 files changed, 299 insertions(+), 204 deletions(-) diff --git a/docs/design/2026-06-12-ocg-sdk-subagent-status.md b/docs/design/2026-06-12-ocg-sdk-subagent-status.md index 18255f728..62ebbb3f2 100644 --- a/docs/design/2026-06-12-ocg-sdk-subagent-status.md +++ b/docs/design/2026-06-12-ocg-sdk-subagent-status.md @@ -196,3 +196,20 @@ already returns LLM-friendly responses, removing the last justification (projection/capping) for server-side OCG code. Full server suite + SDK tests green; two-stub e2e 3/3 on the HttpTask path; republished to mavenLocal. + +## Addendum (2026-06-12, post-live-tuning) + +Live runs against dev OCG surfaced and fixed: +- `start_time`/`end_time` must be full RFC3339 — bare dates got 400s and + pointless Conductor retries; schemas + prompt now say so explicitly. +- Uncapped responses: with server-side capping gone, `max_results: 500` + guidance produced a 162KB response that blew the retriever's context. + `max_results` now carries a schema-level `maximum: 100`, defaults stay + small, `traversal_level` defaults 0, and the prompt carries a retrieval + budget (≤3 distinct keyword queries, no rephrasing — OCG is embedding + search, not a conversation partner). Main-agent examples got explicit + one-retrieval instructions + `max_turns`. NOTE: schema maxima are + LLM-visible constraints, not enforcement — a generic tool-output cap in + agentspan (or an OCG-side response cap) remains the structural fix if + this ever needs to be a guarantee. +- `ocg_code_history` removed from the toolset per user direction. diff --git a/docs/ocg-agent-flow.md b/docs/ocg-agent-flow.md index 83c1e8c82..3fbf6d715 100644 --- a/docs/ocg-agent-flow.md +++ b/docs/ocg-agent-flow.md @@ -1,177 +1,251 @@ -# OCG Sub-Agent +# OCG Retrieval Agents -A retrieval sub-agent over the Open Context Graph (OCG) — message search, -entity lookup, code history, stored memories — that any agent **opts into -from the SDK**. The SDK is the canonical — and only — home of the OCG -integration: system prompt, tool schemas, endpoint routing, and instance -binding. The tools compile to **plain Conductor HTTP tasks** (with path -templating); there is no OCG-specific server code at all. The OCG API -itself returns LLM-friendly responses. +OCG (Open Context Graph) is a retrieval engine over a knowledge graph of +entities — messages, channels, people, tickets — linked by claims and +relationships. It is embedding/keyword search exposed as an HTTP API, not +an LLM. -Nothing is auto-injected: an agent that doesn't declare OCG tools never -makes an OCG call. (The previous design — a server-registered `_ocg_agent` -silently appended to every agent when `OCG_URL` was set — is gone; see -[Migration](#migration-from-auto-expose).) +AgentSpan's OCG integration lives **entirely in the Python SDK** +(`agentspan.agents.ocg`): the retrieval system prompt, the tool schemas, +the endpoint routing, and the instance binding. The tools compile to plain +Conductor HTTP tasks, so **any AgentSpan server runs them with zero +OCG-specific configuration** — no properties, no task types, nothing to +enable. -Design history: [`design/2026-06-12-ocg-sdk-subagent-design.md`](design/2026-06-12-ocg-sdk-subagent-design.md). +OCG is opt-in per agent: an agent that doesn't declare OCG tools never +makes an OCG call. --- -## Using OCG from the SDK +## Two shapes -Delegate retrieval from a main agent: +### 1. Sub-agent — delegate retrieval + +`ocg_agent()` returns an ordinary `Agent` carrying the canned retrieval +prompt and the `ocg_*` tools. Wrap it with `agent_tool()` and the main +agent's LLM sees a single tool; calling it runs the retriever as a +sub-workflow with its own LLM loop, which returns one synthesized, cited +answer. ```python from agentspan.agents import Agent, agent_tool from agentspan.agents.ocg import ocg_agent -retriever = ocg_agent(model="openai/gpt-4o-mini", - url="https://ocg.example.com", credential="OCG_KEY") +retriever = ocg_agent( + model="openai/gpt-4o-mini", + url="https://dev.orkescontextgraph.io", + credential="OCG_PUBLIC_KEY", # secrets-store NAME, never the key +) + main = Agent( name="support", model="openai/gpt-4o", + instructions=( + "Call your retrieval tool exactly once, passing the user's full " + "question. Its answer is complete: when it returns, write your " + "final response as a concise cited brief of what it found." + ), tools=[agent_tool(retriever)], - instructions="...", + max_turns=4, ) ``` -The main agent's LLM sees one tool (named after the retriever); calling it -dispatches the retrieval agent as a SUB_WORKFLOW, which runs its own LLM -loop with the seven `ocg_*` tools and returns a synthesized, cited answer. - -### Multi-instance (data residency / multi-tenancy) - -Each retriever binds its own OCG instance — different agents can target -different graphs: - -```python -us = ocg_agent(name="ocg_us", model="openai/gpt-4o-mini", - url="https://us.ocg.example.com", credential="OCG_US_KEY") -ca = ocg_agent(name="ocg_canada", model="openai/gpt-4o-mini", - url="https://ca.ocg.example.com", credential="OCG_CA_KEY") +```mermaid +sequenceDiagram + autonumber + participant U as User + participant M as Main agent (LLM loop) + participant R as OCG retriever (sub-workflow, own LLM loop) + participant O as OCG instance + + U->>M: "Catch me up on " + M->>M: LLM turn — decides to delegate + M->>R: agent_tool call (SUB_WORKFLOW, request = full question) + loop up to 3 distinct keyword queries + R->>R: LLM turn — forms keyword query + R->>O: POST /api/v1/agent/query (HTTP task) + O-->>R: citations (JSON) + end + R->>R: LLM turn — synthesizes citations + R-->>M: one cited answer (tool result) + M->>M: LLM turn — final brief from the answer + M-->>U: concise cited brief ``` -- `url` — the OCG instance this retriever (and only this retriever) talks - to. Required: every OCG tool binds its own instance; there is no - server-side default. -- `credential` — the **name** of a credential-store entry holding the OCG - bearer token. The server resolves it at execution time; the secret never - appears in Python code, serialized configs, or persisted workflow - definitions. Requires `url`. -- **Names must be distinct per instance.** Inline `agent_tool` child - workflows are registered by agent name — two differently-bound agents - sharing a name overwrite each other's workflow definition. +Choose this shape when retrieval takes judgment — several queries, +neighborhood walks, two-step aggregation. The raw citations stay inside +the retriever's context; the main agent only ever sees the synthesized +answer. -### Custom retrieval agents +### 2. Direct tools — the main agent queries itself -`ocg_agent()` is just an `Agent` factory. For full control take the raw -tools and build your own: +`ocg_tools()` returns the raw `ToolDef`s. Attach them (or a subset) to +your own agent and its LLM issues the queries directly — no sub-workflow +hop, roughly half the tokens for simple lookups, but the raw citations +land in the main agent's context and the retrieval prompting is yours to +write. ```python +from agentspan.agents import Agent from agentspan.agents.ocg import ocg_tools -my_retriever = Agent( - name="retriever", - model="anthropic/claude-haiku-4-5", # your model choice - instructions="My custom retrieval prompt...", - tools=ocg_tools(url="https://us.ocg.example.com", - credential="OCG_US_KEY", - memory=False), # retrieval-only subset +main = Agent( + name="support", + model="openai/gpt-4o-mini", + instructions=( + "Answer using ocg_query, a keyword/embedding retrieval tool (NOT " + "an LLM). Query with specific keywords, never questions. At most " + "one query per topic; then write your final brief from the " + "citations." + ), + tools=ocg_tools( + url="https://dev.orkescontextgraph.io", + credential="OCG_PUBLIC_KEY", + entities=False, # subset switches: query / entities / memory + memory=False, # → ocg_query only + ), + max_turns=6, ) ``` -Subset switches: `query`, `entities` (get_entity + neighborhood), -`code_history`, `memory` (set/reinforce/delete). - -### Pre-flight retrieval - -"Retrieve before the main agent acts" is expressed in user space — make the -retriever the first stage of a sequential pipeline, or instruct the main -agent to call its retrieval tool first. There is no server-side pre-flight -hook. - ---- - -## Server setup - -**None.** OCG tools are plain HTTP tools: no properties, no task types, no -TaskDefs, no beans. Any agentspan server (standalone or embedded) that runs -HTTP tools runs OCG tools. Nothing is registered at boot — retrieval agents -are compiled when a user agent that declares them starts. +```mermaid +sequenceDiagram + autonumber + participant U as User + participant M as Main agent (LLM loop) + participant O as OCG instance + + U->>M: "Catch me up on " + loop one query per topic + M->>M: LLM turn — forms keyword query + M->>O: POST /api/v1/agent/query (HTTP task) + O-->>M: citations (JSON, lands in main context) + end + M->>M: LLM turn — synthesizes citations + M-->>U: concise cited brief +``` --- -## How a call executes - -``` -main agent LLM ── tool call ──▶ SUB_WORKFLOW (retriever) - │ retriever LLM picks e.g. ocg_get_entity - ▼ - enrich script (compile-time JS, dispatch-time eval): - uri = + pathTemplate filled from - the LLM's args (URL-encoded) + queryParams - body = remaining args (consumed args removed) - headers carry the credential placeholder - ▼ - standard Conductor HTTP task - ▼ - OCG instance ─── citations ──▶ retriever LLM +## How a tool call executes + +There is no OCG code on the server. The SDK bakes everything the dispatch +needs into each tool's config at definition time; the compiled workflow's +enrich script (compile-time JavaScript, evaluated at dispatch) turns the +LLM's arguments into a standard Conductor HTTP task. + +```mermaid +sequenceDiagram + autonumber + participant SDK as SDK (ocg.py) + participant C as Compiler (agent start) + participant E as Enrich script (per tool call) + participant H as HTTP task (Conductor) + participant S as Secrets store + participant O as OCG instance + + SDK->>C: ToolDef(tool_type="http", config={url, method,
    pathTemplate, queryParams, headers: {Authorization:
    "Bearer ${OCG_PUBLIC_KEY}"}}) + C->>C: bake config into workflow def
    (placeholder escaped for the host's resolver) + Note over C,E: ...LLM emits a tool call, e.g.
    ocg_get_entity(entity_id="entity_01...", depth=1) + E->>E: uri = url + pathTemplate filled from args (URL-encoded)
    + queryParams present in args + E->>E: body = remaining args (consumed args removed) + E->>H: HTTP task {uri, method, headers, body} + H->>S: resolve credential placeholder by NAME + S-->>H: bearer token (in memory only) + H->>O: HTTPS request + O-->>H: JSON response + H-->>E: response.body → tool result for the LLM ``` Key properties: -- **Per-call instance binding.** Each tool's config carries its instance - `url` (required by the SDK) — different agents target different graphs. -- **Secrets stay server-side.** `credential="OCG_US_KEY"` compiles to a - standard HTTP-tool header placeholder (`#{OCG_US_KEY}` standalone, - `${workflow.secrets.OCG_US_KEY}` embedded), resolved from the credential - store at execution — the same pipeline every `http_tool` uses. -- **LLM-friendly responses are the OCG API's job.** The API returns compact - citation-shaped responses; agentspan applies no OCG-specific projection - or capping. - -## The seven OCG operations - -Endpoint routing lives in the SDK (`agentspan/agents/ocg.py`) and compiles -into each tool's HTTP config (`pathTemplate` + `queryParams` + method): - -| Tool name (LLM-visible) | Endpoint | Method | -| ----------------------- | ---------------------------------------- | -------- | -| `ocg_query` | `/api/v1/agent/query` | `POST` | -| `ocg_get_entity` | `/api/v1/entities/{entity_id}` | `GET` | -| `ocg_neighborhood` | `/api/v1/graph/neighborhood/{entity_id}` | `GET` | -| `ocg_code_history` | `/api/v1/code/history/{repo_id}` | `GET` | -| `ocg_memory_set` | `/api/v1/memories` | `POST` | -| `ocg_memory_reinforce` | `/api/v1/memories/{key}/reinforce` | `POST` | -| `ocg_memory_delete` | `/api/v1/memories/{key}` | `DELETE` | - -Path params (`{entity_id}`, `{key}`, `{repo_id}`) are filled from the -LLM's tool arguments and URL-encoded by the dispatch script; listed query -params are appended when present; everything else becomes the JSON body. +- **Per-tool instance binding.** `url=` is required — every OCG tool set + binds the instance it talks to. Different agents can target different + graphs (e.g. a US retriever and a Canada retriever in one router agent); + agents bound to different instances must have distinct `name`s. +- **Secrets never leave the server.** `credential="OCG_PUBLIC_KEY"` is a + *name*. It compiles to a standard HTTP-tool header placeholder, resolved + from the server's secrets store at execution — the token never appears + in Python code, serialized configs, or workflow definitions. Store it + once (e.g. orkes UI → Secrets, or `PUT /api/secrets/OCG_PUBLIC_KEY`). +- **Path templating is generic.** `pathTemplate`/`queryParams` on an + `http` tool config is a general AgentSpan capability; OCG is simply its + first user. --- -## Migration from auto-expose - -Prior to 2026-06-12, setting `OCG_URL` registered a `_ocg_agent` workflow at -boot and **silently appended** an `ocg_agent` tool to every compiled agent. -That behavior is removed with no flag and no shim: - -| Before | After | -| --- | --- | -| Every agent got `ocg_agent` for free when `OCG_URL` was set | Each agent opts in: `tools=[agent_tool(ocg_agent(model=...))]` | -| `_ocg_agent` workflow registered at boot | No boot-time workflow; retriever compiles when the declaring agent starts | -| `OCG_MODEL` required, boot failed fast without it | Model is a required SDK parameter; `OCG_MODEL` is ignored | -| One server-wide OCG instance | Per-tool `url=`/`credential=` — required; no server-side instance config at all | -| Seven `OCG_*` system tasks + TaskDefs + `agentspan.ocg.*` properties | Plain HTTP tasks — zero OCG server code; `agentspan.ocg.*` properties are gone and ignored | +## The tools + +Endpoint routing lives in `agentspan/agents/ocg.py` and compiles into each +tool's HTTP config: + +| Tool (LLM-visible) | Endpoint | Method | +| ---------------------- | ---------------------------------------- | -------- | +| `ocg_query` | `/api/v1/agent/query` | `POST` | +| `ocg_get_entity` | `/api/v1/entities/{entity_id}` | `GET` | +| `ocg_neighborhood` | `/api/v1/graph/neighborhood/{entity_id}` | `GET` | +| `ocg_memory_set` | `/api/v1/memories` | `POST` | +| `ocg_memory_reinforce` | `/api/v1/memories/{key}/reinforce` | `POST` | +| `ocg_memory_delete` | `/api/v1/memories/{key}` | `DELETE` | + +Path params (`{entity_id}`, `{key}`) are filled from the LLM's tool +arguments and URL-encoded; listed query params are appended when present; +everything else becomes the JSON body. + +Subset switches on `ocg_tools()` / `ocg_agent()`: `query`, `entities` +(get_entity + neighborhood), `memory` (set / reinforce / delete). + +## Keeping the LLM honest + +OCG responses are injected verbatim into the calling LLM's context, so the +schemas and the canned prompt enforce discipline: + +- `max_results` carries a schema-level **`maximum: 100`** (default 10); + the prompt recommends ≤ 25. +- `traversal_level` defaults to **0** (citations only) — each level + multiplies response size. +- `start_time`/`end_time` must be **full RFC3339** + (`2026-06-04T00:00:00Z`); the OCG API rejects bare dates, and the + schemas say so to prevent retry loops. +- The canned retrieval prompt budgets **at most 3 distinct keyword + queries** per request, forbids rephrasing (embedding search returns the + same results for the same intent), anchors relative dates on an + execution-time `__today__`, and instructs keyword-style queries under + ~15 content words. + +`ocg_agent()` defaults to `max_turns=10`; give your *main* agent explicit +retrieval instructions and a small `max_turns` (see the examples) so it +treats the retriever's answer as complete instead of paging for +continuations. + +## Running the examples + +```bash +# one-time: store the OCG bearer token in the server's secrets store +# e.g. orkes UI → Secrets → OCG_PUBLIC_KEY, or +# curl -X PUT http://localhost:8080/api/secrets/OCG_PUBLIC_KEY -d '""' + +cd sdk/python + +# sub-agent shape +OCG_INSTANCE_URL=https://dev.orkescontextgraph.io \ +OCG_CREDENTIAL=OCG_PUBLIC_KEY \ +AGENTSPAN_SERVER_URL=http://localhost:8080/api \ +uv run python examples/116_ocg_subagent.py + +# direct-tools shape +OCG_INSTANCE_URL=https://dev.orkescontextgraph.io \ +OCG_CREDENTIAL=OCG_PUBLIC_KEY \ +AGENTSPAN_SERVER_URL=http://localhost:8080/api \ +uv run python examples/117_ocg_direct_tools.py +``` -Anything that referenced the `_ocg_agent` workflow by name breaks; declare -the agent from the SDK instead. +`AGENTSPAN_SERVER_URL` defaults to the standalone server +(`http://localhost:6767/api`); point it at an embedded host (e.g. +orkes-conductor on 8080) as above. -## Non-SDK clients +## API reference -REST/UI clients inline the equivalent agent JSON: an `agent_tool` whose -`config.agentConfig` carries the retrieval agent — `tools` entries with -`toolType: "ocg_query"` … `"ocg_memory_delete"`, each optionally with -`config: {"url": ..., "credential": ...}`. The canonical prompt is exported -as `agentspan.agents.ocg.OCG_SYSTEM_PROMPT`. +See [Python SDK API Reference → ocg_agent() / ocg_tools()](python-sdk/api-reference.md) +for the full parameter tables. Design history lives under +[`docs/design/`](design/2026-06-12-ocg-sdk-subagent-design.md). diff --git a/docs/python-sdk/api-reference.md b/docs/python-sdk/api-reference.md index f43342a67..5a98bb666 100644 --- a/docs/python-sdk/api-reference.md +++ b/docs/python-sdk/api-reference.md @@ -295,7 +295,7 @@ router = Agent(name="na_support", model="openai/gpt-4o", | `credential` | str | None | Credential-store entry holding the OCG bearer token. Resolved server-side at execution — the secret never appears in Python code or serialized configs. Requires `url` | | `instructions` | str | canned `OCG_SYSTEM_PROMPT` | Override the retrieval prompt | | `max_turns` | int | 10 | Retrieval loop budget | -| `query` / `entities` / `code_history` / `memory` | bool | True | Tool subset switches | +| `query` / `entities` / `memory` | bool | True | Tool subset switches | For a fully custom retrieval agent, take the raw tools instead: diff --git a/e2e/ocg/jira_ocg_smoke.py b/e2e/ocg/jira_ocg_smoke.py index 26daaf9e1..28de09f02 100644 --- a/e2e/ocg/jira_ocg_smoke.py +++ b/e2e/ocg/jira_ocg_smoke.py @@ -48,9 +48,12 @@ name="jira_ocg_smoke", model=MODEL, instructions=( - "You answer questions about the team's work. Delegate every lookup " - "to your retrieval tool and synthesize its cited answer." + "You answer questions about the team's work. Call your retrieval " + "tool exactly once, passing the user's full question. Its answer " + "is complete: when it returns, write your final response as a " + "concise cited brief of what it found." ), + max_turns=4, tools=[agent_tool(retriever)], ) diff --git a/sdk/python/examples/116_ocg_subagent.py b/sdk/python/examples/116_ocg_subagent.py index e020e2cd0..e95d32580 100644 --- a/sdk/python/examples/116_ocg_subagent.py +++ b/sdk/python/examples/116_ocg_subagent.py @@ -9,8 +9,8 @@ ``agent_tool()`` exposes it to the main agent's LLM as a single tool. When the main agent calls it, the sub-agent runs its *own* LLM loop — -it can issue several OCG queries, walk entity neighborhoods, pull code -history — and returns one synthesized, cited answer. The main agent's +it can issue several OCG queries and walk entity neighborhoods — and +returns one synthesized, cited answer. The main agent's context only ever sees that final answer, not the raw graph payloads. Choose this shape when retrieval takes judgment (multi-step lookups, @@ -48,9 +48,7 @@ OCG_INSTANCE_URL = os.environ.get("OCG_INSTANCE_URL") or "" OCG_CREDENTIAL = os.environ.get("OCG_CREDENTIAL") # credential-store name, never the key if not OCG_INSTANCE_URL: - raise SystemExit( - "Set OCG_INSTANCE_URL to your OCG instance, e.g. https://test.contextgraph.io" - ) + raise SystemExit("Set OCG_INSTANCE_URL to your OCG instance, e.g. https://test.contextgraph.io") PROMPT = ( "Catch me up on 'Improvements to Python SDK -- performance, Feature " @@ -71,12 +69,15 @@ def main() -> None: name="jira_ocg_subagent", model=MODEL, instructions=( - "You answer questions about the team's work. Delegate every " - "lookup to your retrieval tool — messages, Jira tickets, and " - "code history all live behind it. Synthesize what it returns " - "into a concise brief and keep its citations." + "You answer questions about the team's work. Call your " + "retrieval tool exactly once, passing the user's full " + "question — messages and Jira tickets all live " + "behind it. Its answer is complete: when it returns, write " + "your final response as a concise brief of what it found, " + "keeping its citations." ), tools=[agent_tool(retriever)], + max_turns=4, ) with AgentRuntime() as runtime: diff --git a/sdk/python/examples/117_ocg_direct_tools.py b/sdk/python/examples/117_ocg_direct_tools.py index 48b0d0ee4..05fa2dbad 100644 --- a/sdk/python/examples/117_ocg_direct_tools.py +++ b/sdk/python/examples/117_ocg_direct_tools.py @@ -21,7 +21,7 @@ ``instructions`` here. This example exposes only ``ocg_query`` (the subset switches turn off -entity/code/memory tools) — the narrowest possible OCG surface. +entity/memory tools) — the narrowest possible OCG surface. Instance binding works exactly as in 116: ``OCG_INSTANCE_URL`` (required) / ``OCG_CREDENTIAL`` env vars. @@ -46,9 +46,7 @@ OCG_INSTANCE_URL = os.environ.get("OCG_INSTANCE_URL") or "" OCG_CREDENTIAL = os.environ.get("OCG_CREDENTIAL") # credential-store name, never the key if not OCG_INSTANCE_URL: - raise SystemExit( - "Set OCG_INSTANCE_URL to your OCG instance, e.g. https://test.contextgraph.io" - ) + raise SystemExit("Set OCG_INSTANCE_URL to your OCG instance, e.g. https://test.contextgraph.io") PROMPT = ( "Catch me up on 'Improvements to Python SDK -- performance, Feature " @@ -63,18 +61,20 @@ def main() -> None: model=MODEL, instructions=( "You answer questions about the team's work using ocg_query, " - "a retrieval tool over a knowledge graph of messages, Jira " - "tickets, and code. Query with specific keywords (ticket " - "titles, component names) — under ~15 content words. If the " - "question spans topics, issue one query per topic, then " - "synthesize the citations into a concise brief." + "a keyword/embedding retrieval tool (NOT an LLM) over a " + "knowledge graph of messages and Jira tickets. Query " + "with specific keywords (ticket titles, component names) — " + "under ~15 content words, never phrased as a question. At " + "most one query per topic, 4 total; never repeat or rephrase " + "a query. When the queries are done, write your final " + "response: a concise brief synthesized from the citations." ), + max_turns=6, tools=ocg_tools( url=OCG_INSTANCE_URL, credential=OCG_CREDENTIAL, query=True, entities=False, - code_history=False, memory=False, ), ) diff --git a/sdk/python/src/agentspan/agents/ocg.py b/sdk/python/src/agentspan/agents/ocg.py index 5e828bb48..313efd8f8 100644 --- a/sdk/python/src/agentspan/agents/ocg.py +++ b/sdk/python/src/agentspan/agents/ocg.py @@ -4,7 +4,7 @@ """OCG (Open Context Graph) retrieval sub-agent. OCG is a retrieval engine over a knowledge graph of entities (messages, -channels, people, code) linked by claims and relationships. This module is +channels, people) linked by claims and relationships. This module is the canonical — and only — definition of the OCG integration: system prompt, tool schemas, endpoint routing, and instance binding all live here. The tools compile to plain Conductor HTTP tasks (with path @@ -69,7 +69,15 @@ You are querying an OCG (Observability Context Graph). It is a RETRIEVAL engine over a knowledge graph of entities (messages, channels, people) -linked by claims and relationships. It is NOT an aggregation engine. +linked by claims and relationships — embedding/keyword search, NOT an LLM. +It is NOT an aggregation engine and NOT a conversation partner: rephrasing +the same intent returns the same results. + +RETRIEVAL BUDGET: make at most 3 queries total, each with a genuinely +DIFFERENT keyword set. Never repeat or lightly rephrase a query. When the +budget is spent — or results start repeating — STOP querying and answer +from what you have. Timestamps must be full RFC3339 +(2026-06-04T00:00:00Z); a bare date is rejected. It can answer: - "Find messages in channel X about Y" @@ -82,13 +90,16 @@ - "Group these by Y" / "Top N by count" - Statistical or comparative questions +RESPONSE SIZE: OCG responses are injected verbatim into your context. +Keep max_results <= 25 and traversal_level = 0 unless you specifically +need graph neighbors. A huge result set will destroy your own context — +narrow the keywords or the time range instead of raising max_results. + For aggregation questions, use a TWO-STEP pattern: - 1. RETRIEVE: ask OCG for the raw set of relevant entities. + 1. RETRIEVE: ask OCG for the relevant entities. - Use specific terms (cluster names, error codes, channel names). - Use start_time (and end_time only for windows closed in the past) to bound the range. - - Set max_results high (e.g. 500) so you get the full set, not a - top-N sample. - Avoid hedging words ("frequently", "across", "occurrences") — OCG ranks by keyword presence, and these are noise tokens. 2. AGGREGATE: count, group, rank yourself from the citation list. @@ -103,7 +114,7 @@ Good (step 1): { "query": "TIMED_OUT health check failure cluster", - "max_results": 500, + "max_results": 25, "start_time": "T00:00:00Z" } (end_time omitted — the range runs through now.) @@ -148,12 +159,29 @@ def _query_tool() -> Dict[str, Any]: "schema": _object( { "query": _prop("string", "Natural-language retrieval query."), - "max_results": _prop("integer", "Max citations to return.", 10), + "max_results": { + "type": "integer", + "description": "Max citations to return. Responses land verbatim " + "in your context — keep this small; 100 is the hard maximum.", + "default": 10, + "maximum": 100, + }, "traversal_level": _prop( - "integer", "0 = citations only, 1 = neighborhood, 2-3 = multi-hop.", 1 + "integer", + "0 = citations only (recommended), 1 = neighborhood, " + "2-3 = multi-hop. Each level multiplies response size.", + 0, + ), + "start_time": _prop( + "string", + "RFC3339 timestamp lower bound (inclusive), e.g. 2026-06-04T00:00:00Z. " + "A bare date like 2026-06-04 is REJECTED. Optional.", + ), + "end_time": _prop( + "string", + "RFC3339 timestamp upper bound (exclusive), e.g. 2026-06-11T00:00:00Z. " + "A bare date is REJECTED. Optional — omit for ranges that run to now.", ), - "start_time": _prop("string", "ISO-8601 lower bound (inclusive). Optional."), - "end_time": _prop("string", "ISO-8601 upper bound (exclusive). Optional."), }, ["query"], ), @@ -197,24 +225,6 @@ def _neighborhood_tool() -> Dict[str, Any]: } -def _code_history_tool() -> Dict[str, Any]: - return { - "name": "ocg_code_history", - "method": "GET", - "path": "/api/v1/code/history/{repo_id}", - "query_params": ["path", "limit"], - "description": "Last N commits that touched a file in an ingested repo.", - "schema": _object( - { - "repo_id": _prop("string", "Ingested repository id."), - "path": _prop("string", "Path within the repo."), - "limit": _prop("integer", "Max commits to return.", 20), - }, - ["repo_id", "path"], - ), - } - - def _memory_set_tool() -> Dict[str, Any]: return { "name": "ocg_memory_set", @@ -302,7 +312,6 @@ def ocg_tools( credential: Optional[str] = None, query: bool = True, entities: bool = True, - code_history: bool = True, memory: bool = True, ) -> List[ToolDef]: """Build the raw OCG :class:`ToolDef` list for a custom retrieval agent. @@ -320,7 +329,6 @@ def ocg_tools( never appears in the serialized config. query: Include ``ocg_query``. entities: Include ``ocg_get_entity`` + ``ocg_neighborhood``. - code_history: Include ``ocg_code_history``. memory: Include ``ocg_memory_set`` / ``ocg_memory_reinforce`` / ``ocg_memory_delete``. @@ -347,8 +355,6 @@ def ocg_tools( if entities: selected.append(_get_entity_tool()) selected.append(_neighborhood_tool()) - if code_history: - selected.append(_code_history_tool()) if memory: selected.append(_memory_set_tool()) selected.append(_memory_reinforce_tool()) @@ -388,7 +394,6 @@ def ocg_agent( max_turns: int = 10, query: bool = True, entities: bool = True, - code_history: bool = True, memory: bool = True, ) -> "Agent": """Build the prebuilt OCG retrieval :class:`Agent`. @@ -406,8 +411,8 @@ def ocg_agent( credential: Credential-store entry for the instance's bearer token. instructions: Override the canned :data:`OCG_SYSTEM_PROMPT`. max_turns: Retrieval loop budget. - query / entities / code_history / memory: Tool subset switches, - forwarded to :func:`ocg_tools`. + query / entities / memory: Tool subset switches, forwarded to + :func:`ocg_tools`. """ from agentspan.agents.agent import Agent @@ -420,7 +425,6 @@ def ocg_agent( credential=credential, query=query, entities=entities, - code_history=code_history, memory=memory, ), max_turns=max_turns, diff --git a/sdk/python/tests/unit/test_ocg.py b/sdk/python/tests/unit/test_ocg.py index 2409e600e..0958469ae 100644 --- a/sdk/python/tests/unit/test_ocg.py +++ b/sdk/python/tests/unit/test_ocg.py @@ -12,7 +12,6 @@ "ocg_query", "ocg_get_entity", "ocg_neighborhood", - "ocg_code_history", "ocg_memory_set", "ocg_memory_reinforce", "ocg_memory_delete", @@ -23,9 +22,9 @@ class TestOcgTools: - def test_default_returns_all_seven(self): + def test_default_returns_all_six(self): tools = ocg_tools(url=URL) - assert len(tools) == 7 + assert len(tools) == 6 assert {t.name for t in tools} == ALL_TOOL_NAMES # OCG tools ARE http tools — they execute as plain Conductor HTTP # tasks; there is no OCG-specific server code. @@ -34,16 +33,15 @@ def test_default_returns_all_seven(self): def test_memory_false_returns_retrieval_only(self): tools = ocg_tools(url=URL, memory=False) - assert len(tools) == 4 + assert len(tools) == 3 assert {t.name for t in tools} == { "ocg_query", "ocg_get_entity", "ocg_neighborhood", - "ocg_code_history", } def test_subset_switches(self): - tools = ocg_tools(url=URL, entities=False, code_history=False, memory=False) + tools = ocg_tools(url=URL, entities=False, memory=False) assert [t.name for t in tools] == ["ocg_query"] def test_url_is_required(self): @@ -74,9 +72,6 @@ def test_endpoint_mapping(self): n = by_name["ocg_neighborhood"].config assert n["pathTemplate"] == "/api/v1/graph/neighborhood/{entity_id}" assert n["queryParams"] == ["depth", "limit"] - c = by_name["ocg_code_history"].config - assert c["pathTemplate"] == "/api/v1/code/history/{repo_id}" - assert c["queryParams"] == ["path", "limit"] r = by_name["ocg_memory_reinforce"].config assert (r["method"], r["pathTemplate"]) == ("POST", "/api/v1/memories/{key}/reinforce") d = by_name["ocg_memory_delete"].config @@ -102,7 +97,8 @@ def test_schemas_have_required_fields(self): by_type = {t.name: t for t in ocg_tools(url=URL)} assert by_type["ocg_query"].input_schema["required"] == ["query"] assert by_type["ocg_get_entity"].input_schema["required"] == ["entity_id"] - assert by_type["ocg_code_history"].input_schema["required"] == ["repo_id", "path"] + # LLM-visible hard ceiling on result-set size. + assert by_type["ocg_query"].input_schema["properties"]["max_results"]["maximum"] == 100 assert by_type["ocg_memory_set"].input_schema["required"] == [ "key", "agent", @@ -154,7 +150,7 @@ def test_instance_binding_flows_to_tools(self): from agentspan.agents.tool import get_tool_def tool_defs = [get_tool_def(t) for t in agent.tools] - assert len(tool_defs) == 7 + assert len(tool_defs) == 6 for td in tool_defs: assert td.config["url"] == "https://us.ocg.example.com" assert td.config["headers"] == {"Authorization": "Bearer ${OCG_US_KEY}"} @@ -162,7 +158,7 @@ def test_instance_binding_flows_to_tools(self): def test_tool_subset_flags_forwarded(self): agent = ocg_agent(model="openai/gpt-4o-mini", url=URL, memory=False) - assert len(agent.tools) == 4 + assert len(agent.tools) == 3 def test_exported_from_agents_package(self): from agentspan.agents import ocg_agent as exported_agent From f5ed5d86090babeb675e00ebff4c65178c69cc70 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Fri, 12 Jun 2026 13:52:14 -0700 Subject: [PATCH 31/61] Remove unneeded files --- .../2026-06-12-ocg-sdk-subagent-design.md | 497 ------------------ .../2026-06-12-ocg-sdk-subagent-status.md | 215 -------- ...2026-06-12-toolspec-selfdescribing-plan.md | 130 ----- .../2026-06-12-ocg-sdk-subagent.md | 76 --- 4 files changed, 918 deletions(-) delete mode 100644 docs/design/2026-06-12-ocg-sdk-subagent-design.md delete mode 100644 docs/design/2026-06-12-ocg-sdk-subagent-status.md delete mode 100644 docs/design/2026-06-12-toolspec-selfdescribing-plan.md delete mode 100644 docs/release-notes/2026-06-12-ocg-sdk-subagent.md diff --git a/docs/design/2026-06-12-ocg-sdk-subagent-design.md b/docs/design/2026-06-12-ocg-sdk-subagent-design.md deleted file mode 100644 index c734376b0..000000000 --- a/docs/design/2026-06-12-ocg-sdk-subagent-design.md +++ /dev/null @@ -1,497 +0,0 @@ -# OCG Sub-Agent via SDK — Fully SDK-Defined, Multi-Instance Design - -**Date:** 2026-06-12 -**Status:** Draft -**Supersedes:** the auto-expose mechanism and server-registered `_ocg_agent` described in `docs/ocg-agent-flow.md` - ---- - -## Overview - -Move the OCG sub-agent out of the server entirely and into the Python SDK: - -- **The SDK owns the agent definition** — system prompt, model, tool schemas, - turn limit — via an `ocg_agent()` factory that returns an ordinary `Agent`, - which users pass to their main agent with the existing `agent_tool()`. -- **The SDK owns the instance binding** — each OCG tool carries the target - OCG base URL and a **credential-store reference** (never the key itself), - so different agents can point at different OCG instances (multi-tenancy, - data residency: a US agent → US graph, a Canada agent → Canada graph). -- **The server owns execution** — the seven `OCG_*` system tasks keep doing - the HTTP calls, secret resolution, field projection, and response capping. - -Deleted outright: auto-expose (`AutoExposedToolsMerger`, -`RegisteredAgent.autoExpose()`, `ExposeAsTool`), the server-registered -`_ocg_agent` workflow, `OcgRegisteredAgent`, `OcgAgentFactory`, and — since -OCG was its only consumer — the `RegisteredAgent` registry -(`RegisteredAgentRegistrar`, `RegisteredAgent`). - -## Motivation - -- **Explicitness.** Tool injection that never appears in user code is - surprising in review and hard to test. After this change, an agent's tool - list in Python is its complete tool list. -- **Multi-instance / multi-tenancy.** A server-wide `OCG_URL` is structurally - single-instance: no agent definition, wherever it lives, can target a - second graph. Per-tool instance binding is the only shape that supports - US/Canada-style sharding and tenant-owned OCG instances. -- **One source of truth.** The previous two-tier design copied the tool - description and schemas from `OcgAgentFactory` into the SDK. With the - server-side definition deleted, the SDK copy *is* the definition. The - "tiers" collapse: customization is just keyword arguments on `ocg_agent()`. -- **Smaller footprint inside orkes-conductor.** AgentSpan is being embedded - into orkes-conductor as a single app. After this change AgentSpan registers - no workflows at boot, mutates no agents, and needs no `OCG_MODEL` — the - host integration shrinks to the task defs plus two *optional* default env - vars (§5). - ---- - -## 1. SDK API - -New module: `sdk/python/src/agentspan/agents/ocg.py`, exported from -`agentspan.agents`. - -### `ocg_agent()` — prebuilt retrieval agent - -```python -from agentspan.agents import Agent, agent_tool -from agentspan.agents.ocg import ocg_agent - -us_retriever = ocg_agent( - name="ocg_us", - url="https://us.ocg.example.com", - credential="OCG_US_KEY", # credential-store name, never the key - model="openai/gpt-4o-mini", -) -ca_retriever = ocg_agent( - name="ocg_canada", - url="https://ca.ocg.example.com", - credential="OCG_CA_KEY", -) - -us_agent = Agent(name="us_support", model="openai/gpt-4o", - tools=[agent_tool(us_retriever)], instructions="...") -ca_agent = Agent(name="ca_support", model="openai/gpt-4o", - tools=[agent_tool(ca_retriever)], instructions="...") - -# Or: one agent that routes by geography — impossible under auto-expose -router = Agent( - name="na_support", model="openai/gpt-4o", - tools=[ - agent_tool(us_retriever, description="Retrieve context for US customers"), - agent_tool(ca_retriever, description="Retrieve context for Canadian customers"), - ], - instructions="...", -) -``` - -```python -def ocg_agent( - *, - name: str = "ocg_agent", - model: str, # required — no silent default (mirrors the old OCG_MODEL fail-fast) - url: str, # required — every tool binds its own instance - credential: Optional[str] = None, # credential-store name for the OCG bearer token - instructions: Optional[str] = None, # defaults to the canned OCG system prompt - max_turns: int = 10, - # tool-subset switches, forwarded to ocg_tools(): - query: bool = True, - entities: bool = True, # ocg_get_entity + ocg_neighborhood - code_history: bool = True, - memory: bool = True, # ocg_memory_set / _reinforce / _delete -) -> Agent: - return Agent( - name=name, - model=model, - instructions=instructions or OCG_SYSTEM_PROMPT, - tools=ocg_tools(url=url, credential=credential, query=query, - entities=entities, code_history=code_history, memory=memory), - max_turns=max_turns, - ) -``` - -The canned `OCG_SYSTEM_PROMPT` and per-tool schemas move verbatim from -`OcgAgentFactory.java` into `ocg.py` — including the behavioral fixes already -shipped there (execution-time "today" anchoring, omit-`end_time`-for-open-ranges -guidance). The SDK becomes their only home. - -**Multi-instance naming requirement:** agents pointing at different OCG -instances MUST have distinct `name`s. Child `agent_tool` workflows are -registered by agent name (`AgentService.registerAgentToolWorkflows()` → -`updateWorkflowDef`), so two differently-configured agents both named -`ocg_agent` would overwrite each other's workflow definition. `ocg_agent()` -docstring carries this warning. (This is a general property of inline agent -tools, not OCG-specific.) - -### `ocg_tools()` — raw tools for custom retrieval agents - -```python -def ocg_tools( - *, - url: Optional[str] = None, - credential: Optional[str] = None, - query: bool = True, - entities: bool = True, - code_history: bool = True, - memory: bool = True, -) -> List[ToolDef]: ... -``` - -Returns up to seven `ToolDef`s with `tool_type="ocg_query"` … -`"ocg_memory_delete"` and the canonical input schemas. For users who want -their own prompt/model/composition and attach OCG tools to an agent they -build themselves. - -### Instance binding rules (revised 2026-06-12: url required) - -- `url` is **required** — every OCG tool binds the instance it talks to. - There is no server-side default instance (`agentspan.ocg.url`/`api-key` - were removed by decision the same day: server-wide instance config is - exactly the single-instance coupling this design exists to kill). -- `credential` names an entry in the credential store; the server resolves - it at execution time and sends `Authorization: Bearer `. The - secret never appears in Python code, serialized configs, or workflow - definitions — identical to the `http_tool` `${NAME}` model, and bounded - the same way (a caller can only name credentials their org has declared). - Omitting it means an unauthenticated instance. - -### Out of scope - -- TypeScript SDK parity — follow-up, same wire format. Until then, the - prompt/schemas live only in the Python SDK; non-SDK clients (REST, UI) - can inline the canonical agent JSON published in the docs. -- Dynamic per-*request* instance selection (one compiled tool, URL chosen at - call time). Instances are fixed at agent-definition time; per-request - routing is expressed as multiple tools (see router example above). - ---- - -## 2. Wire format and server flow - -`ocg_agent()` output is wrapped by the existing `agent_tool()`, so the parent -tool serializes with a full inline `agentConfig` — the standard path, nothing -OCG-specific. Each OCG tool inside it serializes as: - -```json -{ - "name": "ocg_query", - "description": "Query the Open Context Graph ...", - "inputSchema": {"type": "object", "properties": {"query": {...}, ...}, "required": ["query"]}, - "toolType": "ocg_query", - "config": { - "url": "https://us.ocg.example.com", - "credential": "OCG_US_KEY" - } -} -``` - -(`config` omitted entirely for default-instance tools. The serializer also -appends `config.credentials = ["OCG_US_KEY"]` — the existing wire key the -server reads to bound credential resolution for the execution token.) - -Server flow: - -- `registerAgentToolWorkflows()` compiles and registers the retriever child - workflow at start, exactly as for any inline agent tool - (`AgentService.java:1119-1163`). -- `ToolCompiler` already maps the seven `ocg_*` tool types to `OCG_*` task - types (`ToolCompiler.java:136-142`) and already includes `OCG_TOOL_TYPES` - in `serverSideTypes`. **New:** it gains an `ocgConfig` map (the `httpConfig` - pattern at `ToolCompiler.java:342-343`) carrying each OCG tool's - `url`/`credential` through to the task input, with the same credential- - placeholder escaping applied to HTTP/MCP headers. -- The `OCG_*` operations resolve their target per call: - 1. `url` from task config if present, else `OcgProperties.url`, else the - task FAILs with a clear message (§3 validation should have caught this - at start time — the runtime check is the backstop). - 2. Auth: `credential` from task config (resolved via the credential store) - if present, else `OcgProperties.apiKey`, else no auth header. -- Projection and response capping (`responseCapChars`) are unchanged and - shared across all instances. - -### Fail-fast validation at agent start - -In `AgentService.start()` / `startStreaming()`: if any tool (recursively, -including inline agent-tool children) has a type in `OCG_TOOL_TYPES` and -no `config.url` bound, reject the start request: -`"OCG tool 'ocg_query' has no OCG instance bound: set url= on -ocg_agent()/ocg_tools() in the SDK."` - ---- - -## 3. Server changes - -### Delete - -| Item | Notes | -|---|---| -| `compiler/AutoExposedToolsMerger.java` + tests | Auto-expose is gone entirely — no flag, no shim | -| `registry/RegisteredAgent.java` (incl. `ExposeAsTool`), `registry/RegisteredAgentRegistrar.java` + tests | OCG was the registry's only consumer; no boot-time workflow registration remains | -| `ocg/OcgRegisteredAgent.java` | | -| `ocg/OcgAgentFactory.java` | Prompt + schemas move to the SDK (§1); delete after the SDK copy lands, in the same PR, so there is never zero or two sources of truth | -| Merger/registrar call sites and Spring wiring in the compile path | | -| `agentspan.ocg.model` property + its fail-fast boot check | Model is now a required SDK parameter | - -### Modify - -| Item | Change | -|---|---| -| `ocg/operation/*` / `OcgRequestTask` | Read `url`/`credential` from task config with fallback to `OcgProperties` (§2); credential resolution via the existing store machinery | -| `OcgRegisteredTaskDefs` + `OcgRequestTaskConfig` | Registration no longer conditional on `OCG_URL` (there may be no global URL). Gate on `agentspan.ocg.enabled` (default `true`); `url`/`api-key` become the optional *default instance* | -| `AgentService` | Add the §2 start-time validation | -| `OcgProperties` | Drop `model`; document `url`/`apiKey` as default-instance only | - -### Keep unchanged - -- The seven `OCG_*` system task implementations' projection/capping logic. -- `ToolCompiler.TYPE_MAP` `ocg_*` entries — now the cross-repo wire contract - with the SDK. The seven tool-type strings get a Javadoc note that the SDK - depends on them; renames are breaking. - -### Behavior changes (intentional, breaking) - -1. Deployments relying on auto-expose lose the silent `ocg_agent` tool; - agents add `agent_tool(ocg_agent(model=..., ...))` (one import + one line). -2. The `_ocg_agent` workflow is no longer registered; anything referencing it - by name breaks. -3. `OCG_MODEL` is removed and ignored. - -No deprecation shims. Release notes + `docs/ocg-agent-flow.md` rewrite cover -migration. - ---- - -## 4. Pre-flight, revisited - -The original plan's server-side pre-flight was never implemented and stays -that way: "retrieve before the main agent acts" is now expressible in user -space — a sequential pipeline whose first stage is an `ocg_agent()`, or main- -agent instructions to call the retriever first. No server hook. - ---- - -## 5. Orkes-conductor integration - -Context: AgentSpan is being embedded into orkes-conductor as a direct -dependency (single app). [orkes-conductor PR #3673](https://github.com/orkes-io/orkes-conductor/pull/3673) -contains the current integration shim. - -### Tool resolution in `OrkesLLM` - -`OrkesLLM.getToolSpecs()` resolves tools **by name against the Orkes -integration store** — the natural contract for Orkes-native tools, which -*are* integrations/services/task-defs. AgentSpan-compiled tools are -**self-describing** (name, description, `inputSchema` inline in the LLM task -input) and aren't integrations, so the lookup dropped them; `ocg_agent` was -the first casualty, but any SDK-declared `agent_tool`/`http`/`mcp` tool hits -the same wall. - -Options considered: - -| # | Option | Assessment | -|---|---|---| -| 1 | **Schema-presence pass-through** (shipped in PR #3673): `inputSchema != null` ⇒ return the spec as-is, skip integration resolution | Correct for every tool this design produces — both `agent_tool(ocg_agent(...))` and raw `ocg_*` tools ship inline schemas. Weakness: schema presence is a heuristic; if Orkes-native tools ever populate `inputSchema`, resolution is silently skipped, and name collisions with integrations resolve silently in favor of the inline spec | -| 2 | **Explicit marker on `ToolSpec`** (`selfDescribing`), set by AgentSpan's `ToolCompiler.compileToolSpecs()`, branched on in `OrkesLLM` | **Selected.** The principled contract — intent declared, not inferred. Detailed design below | -| 3 | Register AgentSpan agents as Orkes integrations so name-lookup succeeds | **Rejected.** Abuses integration semantics, org-scoping is unanswerable, doesn't generalize to user-declared tools, and resolution would rewrite the compiled spec | -| 4 | AgentSpan ships its own LLM task type bypassing `OrkesLLM` | **Rejected.** Forks the LLM path inside the product we're embedding into; loses Orkes provider/integration management | - -**Decision:** adopt Option 2, with the marker transported in -`ToolSpec.configParams` (an existing `Map` field) rather -than a new model field — a first-class field would require a conductor-oss -release + dependency bump, which we choose not to wait for (decision -2026-06-12). Option 1's heuristic is then replaced outright — no -transition fallback is kept. Verified live: the top-level -`selfDescribing` key is stripped at `ToolSpec` deserialization, while -`configParams.selfDescribing` arrives intact in the bound LLM task input. -The top-level key is still emitted for a possible future first-class -field; nothing depends on it. Implementation detail + paste-ready orkes -changes: [2026-06-12-toolspec-selfdescribing-plan.md](2026-06-12-toolspec-selfdescribing-plan.md). - -### Option 2 in detail — `selfDescribing` on `ToolSpec` - -**The contract.** `selfDescribing = true` means: *this spec is complete as -delivered — name, description, and `inputSchema` are authoritative; consumers -must hand it to the LLM as-is and must not resolve, enrich, or replace it by -name against integrations, services, or task definitions.* It deliberately -says nothing about provenance ("compiled by AgentSpan") — any future producer -of complete inline specs (e.g. UI-authored tools) may set it, and consumers -other than `OrkesLLM` get the same instruction. Post-call routing of the -LLM's tool-call output stays where it already is: the workflow's tool router -(SWITCH), which never depended on integration resolution. - -**Three repos are touched** (the spec travels: AgentSpan compiles tool-spec -maps into the `LLM_CHAT_COMPLETE` task input → Conductor persists them → -`OrkesLLM` deserializes them into `ToolSpec` objects): - -1. **conductor-oss/conductor** — owns the model, `ai` module - (`ai/src/main/java/org/conductoross/conductor/ai/models/ToolSpec.java`, - a Lombok `@Data` POJO: `name`, `type`, `configParams`, - `integrationNames`, `description`, `inputSchema`, `outputSchema`). - Add one field: - - ```java - /** - * When true, this spec is complete as delivered: pass it to the LLM - * as-is. Consumers must not resolve, enrich, or replace it by name - * against integrations, services, or task definitions. Set by - * producers that compile full inline tool specs (e.g. AgentSpan). - */ - private boolean selfDescribing; - ``` - - `boolean` (not `Boolean`): absent on the wire ⇒ `false` ⇒ existing - name-resolution behavior. Lombok generates `isSelfDescribing()`. No - custom Jackson annotations needed — the field round-trips through the - task-input map like every other field, and LLM providers ignore it when - building their native tool definitions (they read name / description / - inputSchema only). - -2. **agentspan** — the producer. `ToolCompiler.compileToolSpecs()` - (`ToolCompiler.java:157`) stamps every compiled spec: - - ```java - spec.put("selfDescribing", true); - ``` - - Unconditional — every AgentSpan tool spec is self-describing by - construction (the compiler always emits complete name + description + - `inputSchema`, for all tool types: `agent_tool`, `http`, `mcp`, `ocg_*`, - worker, …). No per-type logic. - -3. **orkes-conductor** — the consumer. `OrkesLLM.getToolSpecs()` replaces - the PR #3673 heuristic outright: - - ```java - if (toolSpec.isSelfDescribing()) { - // Self-describing spec: complete as delivered — hand it to the - // LLM as-is, never resolve by name. - return List.of(toolSpec); - } - // existing name-based integration resolution below, unchanged - ``` - -**Name-collision semantics become defined behavior:** a self-describing tool -whose name matches a registered integration is passed through — the inline -spec wins, by declared intent rather than by accident of schema presence. -Orkes-native tools without the marker resolve exactly as today, even if they -someday carry schemas — schema presence means nothing to `OrkesLLM` once -the marker branch lands. - -**Rollout and skew.** In the single app, producer (AgentSpan dependency) and -consumer (`OrkesLLM`) upgrade atomically in one deploy, so the only skew is -*data* skew: `LLM_CHAT_COMPLETE` task inputs compiled before the upgrade but -executed after. Two facts bound it: - -- Agent workflow definitions are re-registered on every agent start - (`AgentService.start()` → `updateWorkflowDef`), so every start after the - upgrade compiles fresh specs carrying the marker. Only executions already - mid-flight at deploy time carry markerless specs — those fall through to - name resolution and are dropped, an accepted, hours-bounded window - (no fallback is kept; decision 2026-06-12). -- Conductor's `ObjectMapperProvider` ignores unknown JSON properties, so a - marker-stamped spec deserializes cleanly even against an old `ToolSpec` - (the marker is simply dropped). The required landing order: conductor-oss - field → orkes-conductor dependency bump + consumer branch; until both - land, the PR #3673 heuristic stays in place as the working path. - -**Tests** (fail-first per root `CLAUDE.md`): - -- agentspan unit: every spec returned by `compileToolSpecs()` — across all - tool types — carries `selfDescribing: true` (make it fail first by - asserting a wrong key, e.g. `self_describing`). -- orkes-conductor unit, `OrkesLLM.getToolSpecs()`: - marked spec → passed through, integration lookup **never invoked** - (verify on the mocked integration service); unmarked spec (with or - without schema) → existing resolution path; marked spec whose name - matches a registered integration → inline spec wins. - -### Multi-tenancy fit - -Per-tool instance binding is the only option that matches Orkes' tenancy -model: application properties are app-wide and operator-level, while Orkes -**credential stores are org-scoped**. With this design a tenant self-serves — -they store `OCG_US_KEY` in their org's credential store and reference it from -their agent code; no operator config change, no restart, and credential -resolution is bounded to names their org declared. A named-instances-in- -server-config alternative (`agentspan.ocg.instances.us.url=...`) was -considered and rejected: every tenant onboarding would be an operator-level -config change to an app-wide namespace. - -### Housekeeping in orkes-conductor (follow-up PR) - -- Replace the `application.properties` OCG block from PR #3673: - - ```properties - # ============================================================================= - # OCG (Open Context Graph) Configuration - # ============================================================================= - # OCG agents and tools are declared in user code via the AgentSpan SDK - # (ocg_agent() / ocg_tools()), each binding its own OCG instance URL and - # credential-store reference. The properties below only configure the - # OPTIONAL server-wide default instance, used by tools that don't set url=. - agentspan.ocg.enabled=${OCG_ENABLED:false} - ``` - -- Remove `agentspan.ocg.model=${OCG_MODEL:}` and its fail-fast. -- Replace the PR #3673 `OrkesLLM` heuristic with the `selfDescribing` branch - (no fallback — Option 2 detail above), bumping the conductor-oss - dependency to the version carrying the `ToolSpec` field. - ---- - -## 6. Implementation stages - -Per SDK plan conventions: validation is a separate stage before documentation. - -**Stage 1 — Server: per-instance execution + removals** -1. `OcgRequestTask`/operations: per-call `url`/`credential` resolution with - `OcgProperties` fallback; credential resolved via the existing store path. -2. `ToolCompiler`: `ocgConfig` plumbing with credential-placeholder escaping; - stamp `selfDescribing: true` on every spec in `compileToolSpecs()` (§5). -3. Task-def registration gated on `agentspan.ocg.enabled`, not `url`. -4. Delete auto-expose + registry + `OcgRegisteredAgent`/`OcgAgentFactory`, - strip wiring, drop `agentspan.ocg.model`. -5. Add the start-time "no instance" validation. - -**Stage 2 — SDK: `agentspan/agents/ocg.py`** -1. `OCG_SYSTEM_PROMPT` + seven tool schemas moved verbatim from - `OcgAgentFactory`. -2. `ocg_tools()` (instance binding, subset switches, `credential`-without- - `url` rejection), `ocg_agent()`; export from `agentspan.agents`. - -**Stage 3 — Validation** (before any docs) -- Per root `CLAUDE.md`: every test is written first, made to fail (wrong wire - key, wrong tool-type string, swapped instance URLs), the failure asserted, - then fixed. No LLM-judged validation in e2e — assert on workflow/tool - structure and recorded HTTP traffic, not model output quality. -- Server unit: OCG operation hits per-tool URL when configured, falls back to - `OcgProperties.url`, FAILs cleanly with neither; credential from config - resolves via the store, falls back to `apiKey`. -- Server unit: compile with OCG enabled → **no** OCG tool appears unless - declared (inverse of the old merger test); start-time validation rejects - instanceless OCG tools with the documented message. -- SDK unit: `ocg_agent()` returns an Agent whose tools carry the expected - `tool_type`s and `config`; `ocg_tools(memory=False)` returns exactly 4 - defs; `credential` without `url` raises; `url=None` emits no `config`. -- Server unit: every spec from `compileToolSpecs()` carries - `selfDescribing: true` (§5 test list; the orkes-conductor consumer tests - live in that repo's PR). -- e2e (`sdk/python/e2e/`): **two** stub OCG instances (WireMock or the - `e2e/ocg` harness); a US agent and a Canada agent each with their own - `ocg_agent(...)` — assert each retriever's `OCG_QUERY` traffic lands on its - own stub and only that stub (the multi-tenancy guarantee). Negative e2e: - agent without OCG tools → no OCG dispatch. - -**Stage 4 — Documentation** -- Rewrite `docs/ocg-agent-flow.md` (currently documents auto-expose and the - boot-registered `_ocg_agent`). -- Python SDK API reference for `ocg_agent()` / `ocg_tools()`, including the - multi-instance naming requirement and a canonical agent-JSON snippet for - non-SDK clients. -- Release note for the breaking changes (§3). -- Cross-repo PRs, in strict landing order (§5): conductor-oss - `ToolSpec.selfDescribing` field → orkes-conductor dependency bump + - `OrkesLLM` branch + properties housekeeping. The consumer branch cannot - ship before the field exists (no fallback is kept), and the PR #3673 - heuristic stays in place until it does. diff --git a/docs/design/2026-06-12-ocg-sdk-subagent-status.md b/docs/design/2026-06-12-ocg-sdk-subagent-status.md deleted file mode 100644 index 62ebbb3f2..000000000 --- a/docs/design/2026-06-12-ocg-sdk-subagent-status.md +++ /dev/null @@ -1,215 +0,0 @@ -# OCG SDK Sub-Agent — Implementation Status - -**Date:** 2026-06-12 -**Design:** [2026-06-12-ocg-sdk-subagent-design.md](2026-06-12-ocg-sdk-subagent-design.md) - ---- - -## Accomplished - -### Server — Stage 1 (complete) - -**Per-instance execution:** -- `OcgTarget` record (`ocg/operation/OcgTarget.java`) — the resolved - instance (base URL + Authorization header) an operation runs against. - All seven operations, `OcgRequest`, and `OcgUri` now take `OcgTarget` - instead of `OcgProperties`; operations can no longer reach the default - config. -- `OcgRequestTask` resolves the target per call: reserved task inputs - `__ocg_url` / `__ocg_auth` (compiled from the SDK's `url=` / - `credential=`) win over the `OcgProperties` default; with neither, the - task fails with the documented "no OCG instance" message. Reserved keys - are stripped before operations see the input (so `ocg_memory_set` can't - leak them into request bodies). -- Credential resolution: `OcgCredentialResolver` + - `PlaceholderCredentialResolver` resolve `#{NAME}` placeholders through - the credential store scoped by the execution token (same contract as - `CredentialAwareHttpTask`); resolved values exist only in memory. In - embedded mode the host resolves `${workflow.secrets.NAME}` before the - task starts. An unresolvable placeholder fails the task — it is never - sent as a bearer token. -- `ToolCompiler.buildEnrichTask` bakes `url` + escaped auth placeholder - into the `ocgConfig` entry; the enrich script (both copies in - `JavaScriptBuilder`) merges them into dispatched task inputs. - -**`selfDescribing` marker (agentspan's share of OrkesLLM Option 2):** -- `ToolCompiler.compileToolSpecs()` stamps every compiled spec twice: - top-level `selfDescribing: true` (future first-class field) AND - `configParams.selfDescribing: true` — the copy that survives `ToolSpec` - deserialization today (verified live). Merged into existing MCP/API - `configParams`, never clobbering them. - -**Auto-expose + registered-agent machinery deleted:** -- Deleted: `AutoExposedToolsMerger`, `RegisteredAgent` (+ `ExposeAsTool`), - `RegisteredAgentRegistrar`, `OcgRegisteredAgent`, `OcgAgentFactory` - (prompt/schemas moved verbatim to the SDK), and their tests - (`AutoExposedToolsMergeTest`, `RegisteredAgentBootstrapTest`). -- `AgentCompiler.compile()` is the single entry point; - `compileWithoutAutoExpose` removed (callers updated, incl. - `MultiAgentCompiler`). `RegisteredTaskDefs` / `RegisteredTaskDefsRegistrar` - remain (the task-def half of the registry is still used). - -**Gating + config:** -- `OcgRequestTaskConfig` and `OcgRegisteredTaskDefs` now gate on - `agentspan.ocg.enabled` (default `true`), not on `OCG_URL` — tasks must - exist even with no default instance. -- `OcgProperties`: `model` removed; `enabled` added; `isEnabled()` renamed - `hasDefaultUrl()` (the Lombok-generated `isEnabled()` now reflects the - `enabled` flag). `url`/`api-key` documented as the optional default - instance. -- `application.properties`: OCG block rewritten (adds `OCG_ENABLED`, - drops `OCG_MODEL`, documents SDK-side declaration). - -**Start-time fail-fast:** -- `OcgToolValidator` — rejects agent starts whose OCG tools have no bound - `url` (there is no server-side default instance), and any OCG tool when - `agentspan.ocg.enabled=false`. Walks sub-agents and inline `agent_tool` - children (both typed `AgentConfig` and raw SDK-serialized maps). Wired - into `AgentService.start()`. - -### SDK — Stage 2 (complete) - -- New module `sdk/python/src/agentspan/agents/ocg.py`: - - `OCG_SYSTEM_PROMPT` — moved verbatim from `OcgAgentFactory`, including - the execution-time `${workflow.input.__today__}` date anchor and the - open-range `end_time` guidance. The SDK is now the only home of the - prompt and schemas. - - `ocg_tools(url=, credential=, query=, entities=, code_history=, memory=)` - — the seven raw `ToolDef`s with the canonical schemas; instance binding - lands in each tool's `config` and declares the credential name for - execution-token bounding. `credential` without `url` raises - `ValueError`. - - `ocg_agent(model=, name=, url=, credential=, instructions=, max_turns=, …)` - — prebuilt retrieval `Agent`; `model` is keyword-required (no silent - default). Docstrings carry the multi-instance distinct-name warning. - - Exported from `agentspan.agents`. - -### Validation — Stage 3 (complete) - -Per root `CLAUDE.md`, every new behavior test was written first, run, and -its failure observed before the implementation landed (e.g. the -ToolCompiler instance-config and selfDescribing tests failed 2/16; the -`OcgRequestTask` per-instance tests failed 6/8; the validator tests failed -4/7 against a stub) — then turned green. - -- Server (`conductor-agentspan-server` suite — **all green**, incl. the - full pre-existing suite): - - `OcgRequestTaskTest` (8) — URL override / fallback / no-instance - failure message, pre-resolved + placeholder + unresolvable auth, - default api-key, reserved-input stripping from request bodies. - - `OcgToolValidatorTest` (7) — per-tool url, default fallback, rejection - messages, feature-disabled, inline-child map walk, sub-agent walk. - - `ToolCompilerTest` (+3) — every spec `selfDescribing`, ocgConfig - carries url + `Bearer #{NAME}` (standalone escaping), default-instance - entry stays minimal. - - `AgentCompilerTest` (+1) — inverse of the deleted merger tests: - compile never injects undeclared tools. -- SDK: `tests/unit/test_ocg.py` (16, **all green**) — subset switches, - instance binding in config + declared credentials, `credential`-without- - `url` rejection, schema required-fields, prompt anchor survival, exports, - and an end-to-end serialization test proving the wire shape - (`toolType: ocg_query`, `config.url/credential/credentials`) that the - server-side `ToolCompiler` consumes. -- Note: the full SDK unit suite has ~138 pre-existing failures under the - system Python — identical count on a clean tree; unrelated to this - change. Under the `uv` env, adjacent suites pass. `ruff format` + - `ruff check` clean on all new SDK files. -- **e2e (`sdk/python/e2e/test_suite22_ocg.py`) — 3/3 green** against a - live server running this build: two stub OCG instances; the US-bound - retriever's `OCG_QUERY` traffic landed only on the US stub, the - Canada-bound only on the Canada stub, and the no-OCG agent produced zero - OCG traffic. Validation is recorded HTTP traffic, never LLM-judged. -- The e2e caught and fixed a real pre-existing bug: `OcgRegisteredTaskDefs` - registered TaskDefs under the operation labels (`query`, `memory_set`, …) - while dispatch schedules tasks named `taskType.toLowerCase()` - (`ocg_query`, …) — every OCG dispatch failed with "Cannot find task by - name ocg_query". Fixed (names now derive from `TASK_TYPE`), covered by - `OcgRegisteredTaskDefsTest` (fail-first), verified live. - -### Documentation — Stage 4 (complete) - -- `docs/ocg-agent-flow.md` rewritten for the SDK-declared design: usage, - multi-instance binding, custom retrieval agents, server setup - (`enabled`/default instance, no `OCG_MODEL`), execution flow incl. - credential handling, the operations table, and a migration-from- - auto-expose section. -- `docs/python-sdk/api-reference.md` — new "ocg_agent() / ocg_tools()" - section under Tools (parameters table, multi-instance example, custom - agent example, non-SDK JSON note). -- `docs/release-notes/2026-06-12-ocg-sdk-subagent.md` — breaking changes, - new capabilities, the TaskDef-naming fix, and a migration table. - -### Empirical finding worth knowing (cross-repo) - -On the live server, the *top-level* `selfDescribing` key is dropped when -the spec deserializes through `org.conductoross.conductor.ai.models.ToolSpec` -(no such field; unknown keys ignored) — but `configParams.selfDescribing` -survives intact in the bound LLM task input (verified on a live -execution). That made `configParams` the chosen transport: **no -conductor-oss change or release is needed**; `OrkesLLM` reads the marker -from `getConfigParams()`. - ---- - -## Pending - -1. **orkes-conductor `selfDescribing` consumer — planned, unblocked.** - No conductor-oss work required (the marker rides `configParams`; see - the empirical finding above). Paste-ready plan at - [`2026-06-12-toolspec-selfdescribing-plan.md`](2026-06-12-toolspec-selfdescribing-plan.md): - `OrkesLLM` reads `configParams.selfDescribing` and the heuristic is - removed outright (no fallback, per 2026-06-12 decision), plus the - `application.properties` OCG block update. Lands on PR #3673's branch - whenever ready. A first-class `ToolSpec` field remains optional future - cleanup, nothing depends on it. -2. **TypeScript SDK parity** — explicitly out of scope in the design; - same wire format when picked up. -3. **Commit the work** — everything above is uncommitted on - `feature/OCG_System_Task`. The deployed local server at - `~/.agentspan/server/agentspan-runtime.jar` runs this build (previous - jar preserved as `agentspan-runtime.jar.bak`). - ---- - -## Addendum (2026-06-12, later): server-side default instance removed - -Per user decision, the optional server-wide default OCG instance -(`agentspan.ocg.url` / `api-key`) is gone entirely. `url=` is now required -on `ocg_agent()`/`ocg_tools()`; `OcgProperties` is just -`enabled` + `responseCapChars`; `OcgRequestTask`/`OcgToolValidator` have no -fallback. SDK, examples, smoke, and docs updated; full server suite + SDK -unit tests green; republished to mavenLocal. - -## Addendum (2026-06-12, latest): OCG execution layer deleted — plain HttpTask - -Per user decision, the custom OCG system tasks are gone entirely. The -`runtime/ocg` package (request task, operations, target, validator, -credential resolver, properties, task-def registration) is deleted; the -seven `ocg_*`→`OCG_*` TYPE_MAP entries and all `agentspan.ocg.*` -properties with it. In exchange the generic `http` enrich path gained -optional `pathTemplate`/`queryParams` (URL-encoded fill from LLM args, -consumed args pruned from the body) — proven by GraalJS execution tests. -The SDK's `ocg_tools()` now emits `tool_type="http"` defs carrying -method/pathTemplate/queryParams/headers per operation; auth uses the -standard http-tool `${NAME}` header placeholder. Rationale: the OCG API -already returns LLM-friendly responses, removing the last justification -(projection/capping) for server-side OCG code. Full server suite + SDK -tests green; two-stub e2e 3/3 on the HttpTask path; republished to -mavenLocal. - -## Addendum (2026-06-12, post-live-tuning) - -Live runs against dev OCG surfaced and fixed: -- `start_time`/`end_time` must be full RFC3339 — bare dates got 400s and - pointless Conductor retries; schemas + prompt now say so explicitly. -- Uncapped responses: with server-side capping gone, `max_results: 500` - guidance produced a 162KB response that blew the retriever's context. - `max_results` now carries a schema-level `maximum: 100`, defaults stay - small, `traversal_level` defaults 0, and the prompt carries a retrieval - budget (≤3 distinct keyword queries, no rephrasing — OCG is embedding - search, not a conversation partner). Main-agent examples got explicit - one-retrieval instructions + `max_turns`. NOTE: schema maxima are - LLM-visible constraints, not enforcement — a generic tool-output cap in - agentspan (or an OCG-side response cap) remains the structural fix if - this ever needs to be a guarantee. -- `ocg_code_history` removed from the toolset per user direction. diff --git a/docs/design/2026-06-12-toolspec-selfdescribing-plan.md b/docs/design/2026-06-12-toolspec-selfdescribing-plan.md deleted file mode 100644 index df4df6de6..000000000 --- a/docs/design/2026-06-12-toolspec-selfdescribing-plan.md +++ /dev/null @@ -1,130 +0,0 @@ -# Plan: `selfDescribing` Tool-Spec Marker — orkes-conductor only - -**Date:** 2026-06-12 (revised same day: no conductor-oss dependency) -**Status:** Ready — agentspan's producer side is merged and verified live -**Parent design:** [2026-06-12-ocg-sdk-subagent-design.md](2026-06-12-ocg-sdk-subagent-design.md) §5 - -## Context - -AgentSpan compiles **self-describing** tool specs — name, description, and -`inputSchema` ship inline in the `LLM_CHAT_COMPLETE` task input. Orkes' -`OrkesLLM` worker resolves tools **by name against the integration store**, -so AgentSpan specs were dropped until -[orkes-conductor PR #3673](https://github.com/orkes-io/orkes-conductor/pull/3673) -added a heuristic: `inputSchema != null` ⇒ pass the spec through. - -The durable contract is an explicit marker meaning: *this spec is complete -as delivered — hand it to the LLM as-is; do not resolve, enrich, or replace -it by name against integrations, services, or task definitions.* - -## Transport: `configParams`, not a new field - -A first-class `ToolSpec.selfDescribing` field would require a conductor-oss -change + release + `revConductor` bump (conductor-ai is a Maven dependency -of orkes-conductor, not vendored). **Decision (2026-06-12): don't wait for -that.** `ToolSpec` already has `configParams: Map` — a -generic map that survives deserialization — so the marker rides there. - -AgentSpan (already merged + verified) stamps every compiled spec with both: - -```json -{ - "name": "ocg_query", - "type": "OCG_QUERY", - "selfDescribing": true, // future-proofing only — dropped today - "configParams": {"selfDescribing": true}, // the copy that survives - ... -} -``` - -**Verified live:** in a bound `LLM_CHAT_COMPLETE` task input on a running -server, the top-level key is stripped at `ToolSpec` deserialization -(`ObjectMapperProvider` drops unknown properties) while -`configParams.selfDescribing == true` arrives intact. For MCP/API tools the -marker is merged into their existing `configParams` (mcpServer/baseUrl -entries untouched). - -The top-level key stays in the compiled spec so that if a first-class field -ever lands upstream, consumers can switch to `isSelfDescribing()` with no -producer change — but nothing depends on it. - ---- - -## The orkes-conductor change (unblocked, single repo) - -Branch: the agentspan-embed work already lives on -`feature/agentspan-embed-spike-nich-changes` (PR #3673) — push there, or -stack a follow-up PR if #3673 merges first. - -### Change 1 — `workers/src/main/java/io/orkes/conductor/enterprise/workers/integrations/OrkesLLM.java` - -Replace the PR #3673 heuristic at the top of `getToolSpecs(...)` — the -`inputSchema != null` check is removed entirely (no fallback kept): - -```java - private List getToolSpecs(String orgId, ToolSpec toolSpec) { - if (isSelfDescribing(toolSpec)) { - // Self-describing spec (e.g. compiled by AgentSpan): complete - // as delivered — hand it to the LLM as-is, never resolve by - // name against integrations/services/taskdefs. - return List.of(toolSpec); - } - // existing name-based integration resolution below, unchanged - ... - } - - /** - * The marker rides configParams because ToolSpec has no dedicated - * field; a generic map entry survives deserialization where an - * unknown top-level key would be dropped. - */ - private static boolean isSelfDescribing(ToolSpec toolSpec) { - Map cp = toolSpec.getConfigParams(); - return cp != null && Boolean.TRUE.equals(cp.get("selfDescribing")); - } -``` - -Defined name-collision semantics: a self-describing spec whose name matches -a registered integration passes through (inline wins, by declared intent). - -Note: markerless specs fall through to name resolution and are dropped — -that only affects executions already mid-flight at upgrade time, since -agent workflows re-register on every start and any new start carries the -marker. - -### Change 2 — `server/src/main/resources/application.properties` - -**Delete the OCG block entirely** (whatever its current state — it was -added in PR #3673 and trimmed since). As of the HttpTask refactor, OCG -tools are plain HTTP tools: agentspan reads no `agentspan.ocg.*` property -at all, registers no OCG task types/TaskDefs, and needs no `OCG_ENABLED` -env. Stale `ocg_*`/`query`-style TaskDefs left in the metadata store from -earlier builds are harmless leftovers and may be deleted. - -### Tests (fail-first) - -`OrkesLLM.getToolSpecs()` unit tests: -- spec with `configParams.selfDescribing == true` → passed through; - integration lookup **never invoked** (verify on the mocked integration - service); -- spec without the marker (with or without schema, with or without other - configParams entries) → existing name-resolution path; -- marked spec whose name matches a registered integration → inline wins; -- MCP-shaped spec (`configParams` carrying `mcpServer` **and** the marker) - → passed through with configParams intact. - -After this lands, schema presence means nothing to `OrkesLLM`; the -configParams marker is the sole contract. - ---- - -## Optional future cleanup (conductor-oss, no urgency) - -If/when convenient, add `private boolean selfDescribing` to -`ai/src/main/java/org/conductoross/conductor/ai/models/ToolSpec.java` -(Javadoc: complete-as-delivered semantics, primitive boolean for -backward-compatible absence). AgentSpan already emits the top-level key, so -on a dependency bump `OrkesLLM` can simplify to -`toolSpec.isSelfDescribing() || configParamsMarker(toolSpec)` and the -configParams transport can be retired one release later. None of the above -waits on this. diff --git a/docs/release-notes/2026-06-12-ocg-sdk-subagent.md b/docs/release-notes/2026-06-12-ocg-sdk-subagent.md deleted file mode 100644 index f6181e51e..000000000 --- a/docs/release-notes/2026-06-12-ocg-sdk-subagent.md +++ /dev/null @@ -1,76 +0,0 @@ -# Release Note — OCG Becomes an SDK-Declared Sub-Agent (Breaking) - -**Date:** 2026-06-12 - -OCG (Open Context Graph) retrieval is now **declared in user code via the -Python SDK** and supports **per-agent instance binding** (multi-tenancy / -data residency). The server keeps only the execution layer. - -## Breaking changes - -1. **Auto-expose is removed.** Setting `OCG_URL` no longer injects an - `ocg_agent` tool into every agent. Agents that need retrieval must opt - in: - - ```python - from agentspan.agents import agent_tool - from agentspan.agents.ocg import ocg_agent - - tools=[agent_tool(ocg_agent(model="openai/gpt-4o-mini"))] - ``` - -2. **The `_ocg_agent` workflow is no longer registered at boot.** Anything - referencing it by name breaks; retrieval agents compile when the - declaring agent starts. - -3. **`OCG_MODEL` / `agentspan.ocg.model` is removed and ignored.** The - retrieval agent's model is a required SDK parameter. The boot-time - fail-fast tied to it is gone. - -4. **All OCG server code removed.** OCG tools compile to plain Conductor - HTTP tasks (with path templating, a new generally-available `http` - tool capability). The seven `OCG_*` system task types, their TaskDefs, - and every `agentspan.ocg.*` property (`enabled`/`url`/`api-key`/ - `model`/`response-cap-chars`) are gone and ignored. Every OCG tool - binds its own instance (`url=`, required) from the SDK; the OCG API - itself returns LLM-friendly responses (no server-side projection or - capping). - -## New - -- `agentspan.agents.ocg` — `ocg_agent()` (prebuilt retrieval agent), - `ocg_tools()` (raw tool defs, subset switches), `OCG_SYSTEM_PROMPT`. -- Per-tool instance binding: `ocg_agent(url=..., credential=...)` — `url` - is required; the credential is a credential-store *name*, resolved - server-side at execution; secrets never enter Python code or workflow - definitions. -- HTTP tool path templating: `http`-type tool configs may declare - `pathTemplate` (e.g. `/api/v1/entities/{entity_id}`) and `queryParams`; - the dispatch script fills them from the LLM's arguments (URL-encoded) - and prunes consumed args from the body. OCG uses this; any HTTP tool - can. -- Every compiled tool spec now carries a `selfDescribing: true` marker — - top-level and inside `configParams` (the copy that survives `ToolSpec` - deserialization) — consumed by embedding hosts (orkes-conductor's - `OrkesLLM`) to pass AgentSpan tool specs to the LLM without - integration-store resolution. - -## Fixed - -- OCG TaskDefs are now registered under the names the dispatch script - actually schedules (`ocg_query`, …, `ocg_memory_delete`). Previously they - were registered under the operation labels (`query`, `memory_set`, …), - which failed dynamic-fork dispatch with *"Cannot find task by name - ocg_query in the task definitions."* - -## Migration - -| If you relied on… | Do this instead | -| --- | --- | -| Auto-injected `ocg_agent` on every agent | Add `agent_tool(ocg_agent(model=...))` to each agent that needs retrieval | -| `OCG_MODEL` env var | Pass `model=` to `ocg_agent()` | -| The boot-registered `_ocg_agent` workflow | Declare the retriever from the SDK | -| Server-wide `OCG_URL`/`OCG_API_KEY` | Bind per tool: `ocg_agent(url=..., credential=...)` | - -Details: `docs/ocg-agent-flow.md` and -`docs/design/2026-06-12-ocg-sdk-subagent-design.md`. From 5cef0bef3d6f9d19e04704b7450409e1acda01f1 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Fri, 12 Jun 2026 14:03:14 -0700 Subject: [PATCH 32/61] Rename test --- docs/ocg-agent-flow.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ocg-agent-flow.md b/docs/ocg-agent-flow.md index 3fbf6d715..062419006 100644 --- a/docs/ocg-agent-flow.md +++ b/docs/ocg-agent-flow.md @@ -33,7 +33,7 @@ from agentspan.agents.ocg import ocg_agent retriever = ocg_agent( model="openai/gpt-4o-mini", - url="https://dev.orkescontextgraph.io", + url="https://test.contextgraph.io", credential="OCG_PUBLIC_KEY", # secrets-store NAME, never the key ) @@ -99,7 +99,7 @@ main = Agent( "citations." ), tools=ocg_tools( - url="https://dev.orkescontextgraph.io", + url="https://test.contextgraph.io", credential="OCG_PUBLIC_KEY", entities=False, # subset switches: query / entities / memory memory=False, # → ocg_query only @@ -228,13 +228,13 @@ continuations. cd sdk/python # sub-agent shape -OCG_INSTANCE_URL=https://dev.orkescontextgraph.io \ +OCG_INSTANCE_URL=https://test.contextgraph.io \ OCG_CREDENTIAL=OCG_PUBLIC_KEY \ AGENTSPAN_SERVER_URL=http://localhost:8080/api \ uv run python examples/116_ocg_subagent.py # direct-tools shape -OCG_INSTANCE_URL=https://dev.orkescontextgraph.io \ +OCG_INSTANCE_URL=https://test.contextgraph.io \ OCG_CREDENTIAL=OCG_PUBLIC_KEY \ AGENTSPAN_SERVER_URL=http://localhost:8080/api \ uv run python examples/117_ocg_direct_tools.py From 14c451b065226e203fea12221f1033aaf3138cb4 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Fri, 12 Jun 2026 14:04:21 -0700 Subject: [PATCH 33/61] Remove not needed files --- e2e/ocg/jira_ocg_smoke.py | 63 --------------------------------------- e2e/ocg/requirements.txt | 5 ---- 2 files changed, 68 deletions(-) delete mode 100644 e2e/ocg/jira_ocg_smoke.py delete mode 100644 e2e/ocg/requirements.txt diff --git a/e2e/ocg/jira_ocg_smoke.py b/e2e/ocg/jira_ocg_smoke.py deleted file mode 100644 index 28de09f02..000000000 --- a/e2e/ocg/jira_ocg_smoke.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Jira-over-OCG smoke check. - -OCG is now opt-in from the SDK (auto-expose is gone), so the agent must -declare its retrieval tooling explicitly. This smoke uses the sub-agent -shape; both shapes live as SDK examples: - - - sdk/python/examples/116_ocg_subagent.py (delegate to ocg_agent()) - - sdk/python/examples/117_ocg_direct_tools.py (main agent calls ocg_query) - -Run (from sdk/python, against the embedded orkes server on 8080):: - - OCG_URL=https://test.contextgraph.io \ - OCG_CREDENTIAL=OCG_PUBLIC_KEY \ - uv run python ../../e2e/ocg/jira_ocg_smoke.py - -OCG_CREDENTIAL names a secret in the server's secrets store holding the -instance's bearer token (store it once, e.g. orkes UI -> Secrets, or -PUT /api/secrets/OCG_PUBLIC_KEY). The token itself never appears here. -""" - -import os - -from agentspan.agents import Agent, AgentRuntime, agent_tool -from agentspan.agents.ocg import ocg_agent - -# The agentspan runtime is embedded in the Conductor server, which listens -# on 8080 (not the standalone default 6767). Override via env if needed. -SERVER_URL = os.environ.get("AGENTSPAN_SERVER_URL", "http://localhost:8080/api") -MODEL = os.environ.get("AGENTSPAN_LLM_MODEL", "openai/gpt-4o-mini") - -# Every OCG tool binds the instance it talks to — there is no server-side -# default. OCG_CREDENTIAL optionally names a credential-store entry for the -# instance's bearer token (for unauthenticated/local instances, leave unset). -OCG_URL = os.environ.get("OCG_URL") or "" -OCG_CREDENTIAL = os.environ.get("OCG_CREDENTIAL") -if not OCG_URL: - raise SystemExit("Set OCG_URL to your OCG instance, e.g. https://test.contextgraph.io") - -prompt = ( - "Catch me up on 'Improvements to Python SDK -- performance, Feature " - "parity, logging, metrics etc'. What's the current state, what's " - "underneath it, and what's been changing in the codebase?" -) - -retriever = ocg_agent(name="ocg_retriever", model=MODEL, url=OCG_URL, credential=OCG_CREDENTIAL) - -agent = Agent( - name="jira_ocg_smoke", - model=MODEL, - instructions=( - "You answer questions about the team's work. Call your retrieval " - "tool exactly once, passing the user's full question. Its answer " - "is complete: when it returns, write your final response as a " - "concise cited brief of what it found." - ), - max_turns=4, - tools=[agent_tool(retriever)], -) - -if __name__ == "__main__": - with AgentRuntime(server_url=SERVER_URL) as runtime: - result = runtime.run(agent, prompt) - result.print_result() diff --git a/e2e/ocg/requirements.txt b/e2e/ocg/requirements.txt deleted file mode 100644 index fdd2fa2d3..000000000 --- a/e2e/ocg/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -# From this directory: -# python -m venv venv -# source venv/bin/activate -# pip install -e ../../sdk/python -# No extra deps needed — OCG_QUERY is a server-side system task. From 167e982a76faa748cf58c1bfce0fa368613345cf Mon Sep 17 00:00:00 2001 From: nicholascole Date: Fri, 12 Jun 2026 14:08:01 -0700 Subject: [PATCH 34/61] Delete the empty boot-time registry; doc URL touch-ups RegisteredTaskDefs / RegisteredTaskDefsRegistrar lost their last supplier in the HttpTask refactor (OcgRegisteredTaskDefs) and their siblings in the auto-expose removal (RegisteredAgent/RegisteredAgentRegistrar). The registrar has been a boot no-op since; the runtime now registers nothing at boot. Removes the dev.agentspan.runtime.registry package. Also: flow-doc example URLs point at test.contextgraph.io. Co-Authored-By: Claude Fable 5 --- .../runtime/registry/RegisteredTaskDefs.java | 27 ------- .../registry/RegisteredTaskDefsRegistrar.java | 79 ------------------- 2 files changed, 106 deletions(-) delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java delete mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java deleted file mode 100644 index eafcc8746..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefs.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.registry; - -import java.util.List; - -import com.netflix.conductor.common.metadata.tasks.TaskDef; - -/** - * Marker for a Spring bean that contributes Conductor {@link TaskDef} - * entries to the metadata store at startup. - * - *

    Conductor's dynamic-fork dispatcher resolves tasks by name - * via the TaskDef registry. Custom system task types - * therefore need a matching TaskDef registered before any workflow can - * dispatch them. Beans of this type plug into - * {@link RegisteredTaskDefsRegistrar} and the registration happens - * generically — no per-feature {@code @PostConstruct}.

    - */ -public interface RegisteredTaskDefs { - - /** Task definitions this bean wants registered. */ - List taskDefs(); -} diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java deleted file mode 100644 index 6cb09d9e8..000000000 --- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/registry/RegisteredTaskDefsRegistrar.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2025 AgentSpan - * Licensed under the MIT License. See LICENSE file in the project root for details. - */ - -package dev.agentspan.runtime.registry; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import jakarta.annotation.PostConstruct; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import com.netflix.conductor.common.metadata.tasks.TaskDef; -import com.netflix.conductor.service.MetadataService; - -/** - * Generic registrar that writes every {@link RegisteredTaskDefs}-contributed - * {@link TaskDef} into Conductor's metadata store on startup. - * - *

    Runs at {@code @PostConstruct} time so task defs are written before - * any agent workflow compiles — agent configs frequently reference task - * names whose defs must already exist.

    - */ -@Component -public class RegisteredTaskDefsRegistrar { - - private static final Logger log = LoggerFactory.getLogger(RegisteredTaskDefsRegistrar.class); - - // Service layer, not MetadataDAO: orkes' fork changed the DAO's updateTaskDef - // return type (TaskDef -> String), so DAO calls compiled against OSS break at - // runtime when embedded. MetadataService.updateTaskDef is void in both. - private final MetadataService metadataService; - private final List suppliers; - - @Autowired - public RegisteredTaskDefsRegistrar( - MetadataService metadataService, @Autowired(required = false) List suppliers) { - this.metadataService = metadataService; - this.suppliers = suppliers != null ? suppliers : List.of(); - } - - @PostConstruct - public void registerAll() { - if (suppliers.isEmpty()) { - return; - } - // Upsert via the service layer's create/update split: hosts differ on - // updateTaskDef semantics (OSS DAO upserts; orkes' service throws NOT_FOUND - // for unknown names), so check existence against the full list first. - Set existing = - metadataService.getTaskDefs().stream().map(TaskDef::getName).collect(Collectors.toSet()); - List toCreate = new ArrayList<>(); - int updated = 0; - for (RegisteredTaskDefs supplier : suppliers) { - for (TaskDef def : supplier.taskDefs()) { - if (existing.contains(def.getName())) { - metadataService.updateTaskDef(def); - updated++; - } else { - toCreate.add(def); - } - } - } - if (!toCreate.isEmpty()) { - metadataService.registerTaskDef(toCreate); - } - int count = updated + toCreate.size(); - if (count > 0) { - log.info("Registered {} TaskDef(s) from {} supplier(s)", count, suppliers.size()); - } - } -} From 0ae15c1f0613f9665bc2d2d27946fe717ad77ca7 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Fri, 12 Jun 2026 14:15:07 -0700 Subject: [PATCH 35/61] docs: drop dead design-history link from ocg-agent-flow.md The linked design doc was removed; the dangling link failed mkdocs build --strict. Co-Authored-By: Claude Fable 5 --- docs/ocg-agent-flow.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/ocg-agent-flow.md b/docs/ocg-agent-flow.md index 062419006..b4d184d13 100644 --- a/docs/ocg-agent-flow.md +++ b/docs/ocg-agent-flow.md @@ -247,5 +247,4 @@ orkes-conductor on 8080) as above. ## API reference See [Python SDK API Reference → ocg_agent() / ocg_tools()](python-sdk/api-reference.md) -for the full parameter tables. Design history lives under -[`docs/design/`](design/2026-06-12-ocg-sdk-subagent-design.md). +for the full parameter tables. From a06288e490db1698020511c71b95206f93ca789f Mon Sep 17 00:00:00 2001 From: Viren Baraiya Date: Mon, 15 Jun 2026 09:16:13 -0700 Subject: [PATCH 36/61] Server split (#271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(server): split into conductor-agentspan library + thin server; drop auth stack Invert the AgentSpan/Conductor dependency so AgentSpan is a library Conductor can depend on, with a thin standalone server over it. - Two Gradle modules: conductor-agentspan (library) and conductor-agentspan-server (OSS runtime + bootJar). Old single-module src/ removed. - Conductor artifacts are compileOnly in the library so the host supplies the engine version; the server brings the runnable OSS Conductor. - Pin com.networknt:json-schema-validator to 1.0.73 — conductor-ai -> spring-ai drags in 2.0.0, which dropped com.networknt.schema.JsonSchema and broke conductor-common's JsonSchemaValidator bean (context-refresh hang on startup). - Remove the unused auth/user enforcement stack (UserRepository, ApiKeyRepository, AuthController, AuthUserSeeder, AuthProperties) and the users/api_keys tables; auth was off by default and never enforced. - Collapse the request principal to a String userId, delete User, and relocate the carrier from auth/ to a context/ package; AuthFilter sets an anonymous userId. - Add design docs under server/docs/. Full build green: 666 tests, 0 failures. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(python-sdk): tokenize CLI command so full command lines work The run_command tool (cli_allowed_commands) assumed `command` was the bare executable, but LLMs routinely pass the whole command line ("gh repo list --limit 5"). os.path.basename then returned the entire string, failing the whitelist check ("Command 'gh repo list ...' is not allowed"), and non-shell exec tried to run a binary named after the full line. Tokenize with shlex: validate on the executable, exec tokens + args. Adds tests covering the full-command-line case. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(ui): rename Credentials sidebar entry to Secrets Match the server-side secrets rename (/api/secrets). Label and id updated; it already links to SECRETS_URL. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(ui): remove dead secrets login gate; rebuild bundle for /api/secrets AuthController and the auth-enforcement stack were removed server-side (OSS runs anonymous; an embedding host like orkes supplies its own auth), so the secrets page's login flow is obsolete and pointed at the deleted /api/auth/login. - Drop LoginDialog, useSecretAuth, and useLogin (and the LoginRequest/Response types and their tests); SecretsPage runs anonymously with no token. - Rebuild the served UI bundle so the browser stops calling the old /api/credentials endpoint and uses /api/secrets/v2. Verified: 0 /auth/login and 0 /credentials fetch paths in the new bundle. UI tests: 455 passed, 1 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) * refactor(server): extract storage/secret SPIs to library, push impls to server Phase 1 of the library split: conductor-agentspan now defines the SPI contracts + logic; the concrete implementations live in conductor-agentspan-server, so an embedding host (orkes) can supply its own. - New dev.agentspan.runtime.spi package in the library: CredentialStoreProvider, SkillPackageStore (+ StoredSkillPackage), and a new SecretOutputMasker. - Impls + infra moved to the server module: EncryptedDbCredentialStoreProvider, FileSystem/ConductorPayload skill stores, MasterKeyConfig, CredentialDataSourceConfig, CredentialSchemaMigrator, CredentialEnvSeeder, schema-credentials*.sql, and the no-op masker (CredentialOutputMasker -> NoOpSecretOutputMasker). - Library logic (CredentialResolutionService, SkillRegistryService, CredentialMaskingResponseAdvice) depends only on the SPI interfaces; verified the library main references no concrete impl. - Removed SkillRegistryService's convenience constructor that instantiated FileSystemSkillPackageStore; tests pass a store explicitly. Decisions: ExecutionTokenService stays in the library (internal token protocol, not a swappable backend); credential DataSource keeps @Primary (needed standalone; embedding-time change deferred to Phase 4). Wiring still flows through @ComponentScan; Phase 2 converts to auto-config with @ConditionalOnMissingBean. Full build green: 666 tests, 0 failures. Co-Authored-By: Claude Opus 4.8 (1M context) * refactor(server): self-register the library via Spring auto-configuration Phase 2 of the library split: conductor-agentspan now registers its beans through Spring Boot auto-configuration, so an embedding host (orkes) gets them without adding dev.agentspan to its own component scan. - New AgentSpanAutoConfiguration (@AutoConfiguration) component-scans dev.agentspan.runtime (excluding AgentRuntime and itself), exported via META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. - AgentRuntime now scans only the Conductor packages; AgentSpan beans come from the auto-config. No double-scan. - Split-package design makes the scan register library beans always and server-extra beans (default SPI impls, web config) only when present — so standalone gets everything and an embedding host supplies its own SPI impls. Phase 3 verification: full build green (666 tests, 0 failures); the standalone bootJar boots in ~3s and serves (/api/agent/list 200, /api/secrets/v2 200, /actuator/health 200), confirming the auto-config imports file is read from the nested library jar in the fat jar. Deferred to Phase 4 (embedding): converting the @Primary task/listener/datasource overrides to opt-in. They only conflict when embedded in orkes. Co-Authored-By: Claude Opus 4.8 (1M context) * build(server): publish modules to Maven Central; tidy library deps Publish conductor-agentspan and conductor-agentspan-server to Maven Central under the org.conductoross.conductor group, mirroring the Java SDK's setup. - Add com.vanniktech.maven.publish (apply-false at root, applied per module) with full POM metadata, coordinates org.conductoross.conductor::, and conditional signing. Group/version set on subprojects; version from gradle.properties (default 0.1.0) or -Pversion=X in CI. - Library jar publishes as conductor-agentspan(.jar); server's plain jar as conductor-agentspan-server(.jar) without the "-plain" classifier. The runnable fat jar (agentspan-runtime.jar) is still released via the S3/GitHub workflow. - New release-server-maven.yml: workflow_dispatch with version, maven-central environment, publishAndReleaseToMavenCentral (publishes both modules), reusing the SONATYPE_*/SIGNING_* secrets. - Javadoc made lenient (Xdoclint:none + failOnError=false) so Lombok's onConstructor_ doesn't abort the Central-required javadoc jar. Dependency hygiene for the published library artifact: - Drop dead impl-layer deps from the library (spring-jdbc, HikariCP, sqlite-jdbc, spring-security-crypto) — moved to the server with the SPI impls in Phase 1 / removed with the auth stack. - Library exports only the slf4j facade (not a log4j2 binding) and no springdoc; both are runtime/app concerns now declared explicitly in the server. Verified: clean build green (666 tests); publishToMavenLocal produces correct coordinates/POM/sources/javadoc for both modules; bootJar boots and serves /api-docs, /api/agent/list, /api/secrets/v2 (all 200). Co-Authored-By: Claude Opus 4.8 (1M context) * feat(java-sdk): migrate to Conductor client, fix e2e degradation, add framework coverage ## Core changes - Migrate worker layer to official conductor-client 5.0.1 (TaskRunnerConfigurer + Worker with lease-extend heartbeat); delete hand-rolled WorkerHttp - Use io.orkes ApiClient with native key/secret auth; remove serverUrl/authKey/authSecret from AgentConfig (they don't belong there) - Move all public/internal classes to org.conductoross.conductor.ai / .internal packages - Add AgentClient (peer of WorkflowClient) for /api/agent/* control-plane - Add SseClient riding ApiClient.buildCall for SSE streaming - Remove HttpApi.java entirely; all HTTP rides the conductor client ## e2e bug fixes - Fix stateful-domain worker bug: domain change on re-registration now triggers runner rebuild so workers poll the correct per-execution queue (not the default) - Remove redundant no-domain prepareWorkers call from runAsync/streamAsync that caused the domain-aware registration to be skipped ## e2e performance fixes (3h → 2m 44s) - Fix MIN_WORKER_THREADS=16 ignoring configured threadCount; use MIN_THREADS_PER_WORKER=1 so AgentConfig(100,1) gets 1 thread, not 16 (was 160 req/s to SQLite server) - Add connectTimeout/readTimeout/writeTimeout to default ApiClient construction (was infinite; slow server responses blocked forever) - waitForResult: fail after 10 consecutive errors with root cause in message instead of silently spinning to 600s timeout - AgentRuntime.close() now calls conductorClient.shutdown() to evict OkHttp pool - maxParallelForks=3 for e2e task: I/O-bound suites with unique names run concurrently ## Test coverage - 204 unit tests (up from 39), 0 failures - 340 e2e tests across 25 suites, 0 failures, 0 skipped - New: WorkerManagerDomainTest (domain rebuild regression, proven counterfactual) - New: WorkerManagerThreadCountTest (thread formula regression, proven counterfactual) - New: AgentHandleErrorTest (fast-fail regression, @Timeout(5) proves counterfactual) - New: AdkBridgeTest (ADK serialization unit, server-free) - New: Suite11bOpenAIAgent (OpenAI framework e2e: tagging + compile + runtime) ## Formatting - Add spotless with palantirJavaFormat 2.50.0; apply to src/main, src/test, e2e Co-Authored-By: Claude Sonnet 4.6 (1M context) * updates * fix(ci): correct server JAR path in e2e workflows bootJar runs in server/ working-directory, so the jar lands at conductor-agentspan-server/build/libs/ not build/libs/. Upload and download paths were pointing at the wrong location, causing all e2e jobs to fail with "Artifact not found: server-jar". Co-Authored-By: Claude Sonnet 4.6 (1M context) * refactor(java-sdk/spring): remove redundant auth props; depend on conductor-client-spring AgentspanProperties had serverUrl/authKey/authSecret that duplicated what conductor-client-spring's OrkesConductorClientAutoConfiguration already handles via conductor.* properties. Removed them. AgentspanAutoConfiguration no longer creates the ApiClient bean — it takes the one wired by conductor-client-spring (after = OrkesConductorClientAutoConfiguration). Our module now only owns the two Agentspan-specific knobs: workerPollIntervalMs and workerThreadCount. Add conductor-client-spring 5.0.1 as an api dependency so users get the ApiClient auto-configuration transitively. Users configure connectivity once: conductor.root-uri=http://localhost:6767/api conductor.security.client.key-id=my-key # optional conductor.security.client.secret=my-secret # optional And Agentspan worker tuning separately: agentspan.worker-poll-interval-ms=100 agentspan.worker-thread-count=1 Also extend spotless coverage to spring/src/**/*.java. Co-Authored-By: Claude Sonnet 4.6 (1M context) * perf(e2e): parallelize Python and TypeScript e2e to target <5 min Python: -n 1 → -n 3 --dist=loadgroup - pytest-xdist was already installed but unused (single worker) - xdist_group markers already protect credential suites (suite2/3/4/5 serialize within "credentials" group; suite16 serializes within "cli-skills") — loadgroup enforces this correctly - 13 independent suites spread across 3 workers; ~3× wall-clock reduction TypeScript: maxForks 2 → 3 - All 23 suites use unique credential names so concurrent forks don't conflict (E2E_TS_CRED_A/B, GITHUB_TOKEN, MCP_AUTH_KEY_TS, HTTP_AUTH_KEY_TS) - Suites 17/18 (the previously-cited "heavy" ones) don't use credentials - Additional fork reduces the sequential tail per worker Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): add mkdocs documentation for Java SDK 14 pages covering the full public API: sdk/java/docs/ ├── mkdocs.yml — standalone site config (serves independently) ├── index.md — overview, install, hello world ├── getting-started.md — setup, first agent, first tool, streaming ├── spring-boot.md — auto-configuration, properties, bean overrides ├── api-reference.md — complete method signatures for all public classes ├── concepts/ │ ├── agents.md — Agent.builder() full reference, AgentRuntime API │ ├── tools.md — @Tool, HTTP, MCP, CLI, human, PDF, media, agent tools │ ├── multi-agent.md — all 7 strategies with examples │ ├── guardrails.md — regex, LLM, custom; positions; OnFail actions │ ├── termination.md — MaxMessage, StopMessage, TextMention, TokenUsage, composition │ ├── scheduling.md — cron deploy, Schedule.builder(), Schedules API │ └── skills.md — SKILL.md format, Skill.skill(), loadSkills() └── frameworks/ ├── langchain4j.md — @Tool POJO bridge, LangChainBridge ├── openai.md — OpenAIAgent builder, handoffs, structured output └── google-adk.md — AdkBridge.toAgentspan(), agentBuilder(), mapping table docs/java-sdk → symlink to sdk/java/docs so root mkdocs.yml can include it. Root mkdocs.yml gains a "Java SDK" nav section before Reference. Standalone: cd sdk/java/docs && mkdocs serve Main site: mkdocs serve (from repo root) Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): add AgentClient control-plane API reference Covers all 5 public methods with exact HTTP verbs, paths, and field-level input/output documentation sourced from the server's StartRequest, CompileResponse, StartResponse, AgentConfig, and ToolConfig DTOs: POST /api/agent/compile → CompileResponse (workflowDef, requiredWorkers) POST /api/agent/deploy → StartResponse (agentName, requiredWorkers) POST /api/agent/start → StartResponse (executionId, agentName, requiredWorkers) GET /api/agent/{id}/status → status, isComplete, isWaiting, pendingTool shape POST /api/agent/{id}/respond → HITL resume (204) GET /api/workflow/{id} → raw Conductor workflow (via WorkflowClient) Also documents AgentConfig and ToolConfig field tables. Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): clarify getWorkflow is post-completion enrichment, not polling Co-Authored-By: Claude Sonnet 4.6 (1M context) * refactor(java-sdk): remove getWorkflow from AgentClient; use WorkflowClient directly AgentClient owns the agentspan-proprietary /api/agent/* control-plane. getWorkflow was fetching from the standard Conductor /api/workflow/* endpoint via WorkflowClient — that belongs with WorkflowClient, not AgentClient. Changes: - AgentClient: remove getWorkflow(), workflowClient field, Workflow/WorkflowClient imports - AgentHandle: inject WorkflowClient directly; call workflowClient.getWorkflow() with the typed Workflow/Task objects — eliminates the JSON roundtrip (Workflow → JSON string → Map) that the old delegation required - AgentRuntime: construct WorkflowClient alongside AgentClient; pass it to every new AgentHandle(...) Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): add method summary table to AgentClient API reference Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): sync AgentClient API doc with current implementation - Remove getWorkflow from AgentClient methods table (deleted in previous refactor) - Rename compile/deploy/start entries to match actual method names (compileAgent etc.) - Reclassify getWorkflow section as 'WorkflowClient usage' — it is not an AgentClient method; AgentHandle calls WorkflowClient.getWorkflow directly - Tighten AgentClient javadoc: explicit scope (five endpoints only), note that standard Conductor endpoints go through their own typed clients Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): rewrite AgentClient API doc with structural proof Every method now shows exact field names sourced from the server DTOs and verified against how AgentRuntime/AgentHandle parse the response: compile → server CompileResponse (workflowDef, requiredWorkers) SDK: plan() returns the raw map; callers get("workflowDef") deploy → server StartResponse (agentName, requiredWorkers; no executionId) SDK: deploy() reads resp.getOrDefault("agentName", agent.getName()) start → server StartResponse (executionId, agentName, requiredWorkers) SDK: extractExecutionId() tries executionId → workflowId → id → correlationId getAgentStatus → plain Map built from live Workflow object (not a DTO) source fields documented (workflow.getStatus().name(), etc.) respond → void; SDK approve/reject/respond mapped to exact body shapes Removed incorrect "CompileResponse" class reference in the table; corrected return type to show the actual JSON shape. Co-Authored-By: Claude Sonnet 4.6 (1M context) * refactor(java-sdk): replace Map returns in AgentClient with typed POJOs Every AgentClient method now has a proper return type instead of Map: compileAgent() → CompileResponse { workflowDef, requiredWorkers } deployAgent() → StartResponse { executionId=null, agentName, requiredWorkers } startAgent() → StartResponse { executionId, agentName, requiredWorkers } getAgentStatus() → AgentStatusResponse { status, isComplete, isRunning, isWaiting, output, reasonForIncompletion, pendingTool } respond() → void (unchanged) New classes: model/CompileResponse.java — public (returned by AgentRuntime.plan()) internal/StartResponse.java — internal; @JsonAlias for legacy executionId keys internal/AgentStatusResponse.java — internal; polled by AgentHandle internal/PendingTool.java — internal; nested in AgentStatusResponse Callers updated: AgentRuntime.plan() → returns CompileResponse AgentRuntime.deploy() → reads resp.getAgentName() instead of Map.getOrDefault() AgentRuntime.startAsync() → reads resp.getExecutionId() directly; extractExecutionId() deleted AgentHandle.waitForResult / isWaiting / waitUntilWaiting / buildResult → typed getters AgentStream.waitForResult / buildResultFromStatus → typed getters Agentspan.plan() → returns CompileResponse BaseTest.getAgentDef(CompileResponse) → uses plan.getWorkflowDef() 15 e2e suites → Map plan = → CompileResponse plan = Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): sync AgentClient API doc with POJO refactor Update all stale references from the Map era: - Methods table now shows Java return types (CompileResponse, StartResponse, AgentStatusResponse, void) instead of JSON shape strings - compileAgent: response section uses CompileResponse getters, not plan.get() - deployAgent: uses resp.getAgentName() not Map.getOrDefault() - startAgent: extractExecutionId() reference removed (deleted); shows StartResponse.getExecutionId() + @JsonAlias legacy key handling - getAgentStatus: response described as AgentStatusResponse (typed POJO), not 'plain Map'; PendingTool fields added with getters - respond: return type shown as void, not '204 No Content' - WorkflowClient section: notes Workflow/Task typed objects, not Map walk Co-Authored-By: Claude Sonnet 4.6 (1M context) * refactor(java-sdk): type AgentClient input Maps as AgentRequest and RespondBody AgentClient no longer takes Map as input: compileAgent(AgentRequest) — was Map deployAgent(AgentRequest) — was Map startAgent(AgentRequest) — was Map respond(String, RespondBody) — was (String, Map) New internal classes: AgentRequest — matches server StartRequest field-for-field: - agentConfig / framework + rawConfig (mutually exclusive, native vs framework) - prompt, sessionId, runId, staticPlan (@JsonProperty("static_plan")) - media, context, idempotencyKey, credentials, skillRef, timeoutSeconds - @JsonInclude(NON_NULL) — null fields omitted from wire - Factory: AgentRequest.nativeAgent(map) / frameworkAgent(fw, map) RespondBody — replaces Map for /respond: - RespondBody.approve() / approve(comment) / reject(reason) / of(map) - @JsonAnyGetter flattens extra fields to top level (MANUAL strategy) AgentRuntime: all three payload-building HashMap blocks replaced with agentRequest(agent, serialized).prompt(...).runId(...).build() AgentHandle, AgentStream: approveBody/rejectBody helpers deleted; all respond calls use RespondBody factories. Structural proof: AgentRequest @JsonProperty names verified against server StartRequest field names (including "static_plan" alias). docs(java-sdk): update agent-client-api.md with AgentRequest/RespondBody - Methods table shows Java input types - AgentRequest field mapping table (SDK → JSON key → server field) - RespondBody factory → wire JSON table - static_plan mismatch explained (@JsonProperty on both sides) Co-Authored-By: Claude Sonnet 4.6 (1M context) * refactor(java-sdk): replace Map fields in AgentRequest with Agent and Plan types AgentRequest now holds SDK types instead of pre-serialized Maps: agentConfig: Agent (was Map) rawConfig: Agent (was Map) staticPlan: Plan (was Map) Serialization to the server's wire format is handled by two new Jackson JsonSerializer inner classes: AgentConfigSerializer.AsJson — applied via @JsonSerialize on agentConfig and rawConfig fields; calls serialize(agent) and writes the result so the server's AgentConfig DTO sees the correct camelCase map. Plan.AsJson — applied via @JsonSerialize on staticPlan; calls plan.toJson() so the server's PAC consumes the same format as Python/TypeScript. AgentRuntime: serialize() calls removed from plan/deploy/startAsync. The AgentConfigSerializer field and instance are gone from the runtime — serialization now happens inside AgentRequest/Jackson, not at the call site. agentRequest(Agent) no longer takes a pre-serialized Map. context: Map stays — intentionally free-form pass-through. docs: AgentRequest table updated with Java types and serializer column. Co-Authored-By: Claude Sonnet 4.6 (1M context) * refactor(java-sdk): eliminate agentConfig/rawConfig duplication; add Framework enum AgentRequest previously had two Agent-typed fields (agentConfig, rawConfig) that were mutually exclusive and duplicated the @JsonSerialize annotation. Replaced with a single agent: Agent field + Framework enum discriminator. AgentRequest.Serializer handles the key decision: framework == null → "agentConfig": serialize(agent) framework != null → "framework": fw.wireValue(), "rawConfig": serialize(agent) All JSON logic is now in one place. @JsonProperty / @JsonInclude / @JsonSerialize annotations removed from individual fields — the Serializer owns everything. Framework enum (enums package, public): OPENAI("openai"), GOOGLE_ADK("google_adk"), SKILL("skill") @JsonValue on wireValue() → serializes to the server-expected string Framework.of(String) → Optional for safe conversion AgentRuntime.agentRequest(Agent): Framework.of(agent.getFramework()) .map(fw -> AgentRequest.frameworkAgent(fw, agent)) .orElseGet(() -> AgentRequest.nativeAgent(agent)) Co-Authored-By: Claude Sonnet 4.6 (1M context) * feat(java-sdk): add missing Framework enum values to match all server normalizers Framework.of(String) already returns Optional.empty() for unknown values, so adding these is safe — existing code that sets langchain/langgraph via .framework(string) on Agent will now resolve to the typed enum instead of falling through to the native path. Added: LANGCHAIN("langchain") — LangChainNormalizer LANGGRAPH("langgraph") — LangGraphNormalizer VERCEL_AI("vercel_ai") — VercelAINormalizer CLAUDE_AGENT_SDK("claude_agent_sdk") — ClaudeAgentSdkNormalizer Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): sync agent-client-api.md with Framework enum + AgentRequest refactors - AgentRequest section rewritten: single 'agent: Agent' field replaces the old agentConfig/rawConfig duplication; Serializer table shows how it writes 'agentConfig' vs 'framework'+'rawConfig' - Framework enum table: all 7 values with wire string and server normalizer - framework type corrected: String → Framework enum throughout - Removed stale references: AgentConfigSerializer.AsJson/@JsonSerialize on fields, @JsonInclude(NON_NULL) on class, Plan.AsJson on field — all of these are now inside AgentRequest.Serializer, not annotations - compileAgent code example updated to use Framework.OPENAI enum constant - Structural proof updated to reference gen.writeObjectField('static_plan',...) instead of @JsonProperty annotation Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): add AgentRuntime API reference Covers the full public surface of AgentRuntime — the primary SDK entry point: Constructors (4 overloads) + environment variables table ApiClient factories: clientFromEnv / client(url) / client(url, key, secret) - connectTimeout=10s, readTimeout=30s, writeTimeout=30s baked in run / runAsync — sync + async; Plan overload for PLAN_EXECUTE start / startAsync — fire-and-forget; AgentHandle methods table stream / streamAsync — SSE event iteration; AgentEvent fields table; event-targeted approve/reject for HITL sub-executions plan — CompileResponse; delegates to AgentClient.compileAgent deploy / deployAsync — idempotent registration; Schedule reconciliation overload serve — blocking worker mode; SIGTERM shutdown hook resume / resumeAsync — crash recovery / reconnect to existing execution schedules — lazy Schedules accessor shutdown / close — stops workers + releases OkHttp pool AgentConfig table: workerPollIntervalMs, workerThreadCount + env vars Thread safety: one shared instance per app, shutdown hook pattern Wired into standalone mkdocs.yml and root mkdocs.yml under "API Reference". Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): add Agent field reference + fix missing Javadoc Agent.java — three builder methods that had no Javadoc: handoffs(List/varargs) — SWARM handoff triggers; reference OnTextMention/OnToolResult allowedTransitions(Map) — SWARM transfer restrictions framework(String) — reference Framework enum; explain wire value convention frameworkConfig(Map) — explain spread-at-top-level behaviour sessionId field-level comment: explicitly notes it is NOT in agentConfig (execution parameter, not compilation parameter) — the one subtle field that users most commonly misplace. agent-structure.md — complete field-by-field reference: - 40-row mapping table: Java field → JSON key → server AgentConfig → Python - "NOT in agentConfig" table: sessionId, stateful, framework, frameworkConfig, 4 callback functions — documents WHY each is excluded - "In server but not Java" table: description, memory, reasoningEffort, maskedFields, contextWindowBudget - "In Python but not Java" table: memory, dependencies, reasoning_effort, masked_fields - Serialization rules: strategy emission guard, framework dispatch paths, synthesize default, plannerContext validation, callback serialization - Defaults comparison: Java builder vs server Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): correct gate and enable_planning — both exist in Python gate (TextGate) and enable_planning are present in the Python Agent class (sdk/python/src/agentspan/agents/agent.py lines 360, 369, 527, 589). The 'Not in Python' table was wrong — correct to show Python equivalents: gate → gate (TextGate) enablePlanning → enable_planning Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): validate and correct 'not in Java' field claims Verified by reading: sdk/python/src/agentspan/agents/agent.py (field definitions) sdk/python/src/agentspan/agents/config_serializer.py (serialization) server/conductor-agentspan/src/main/java/.../AgentConfig.java Corrections: memory, reasoning_effort, masked_fields, context_window_budget — exist in Python Agent AND are serialized to the server; genuine Java gaps; moved to 'In Python (and server) but not in Java' table with JSON keys description — exists in server AgentConfig ONLY; neither Java nor Python Agent exposes it (set by UI/platform); moved to its own table gate / enable_planning — already corrected in previous commit Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): correct serialization notes — stateful/sessionId/callbacks ARE emitted The 'Fields NOT in compiled agentConfig' section was wrong about 6 of 8 entries. Verified by reading AgentConfigSerializer.java: sessionId → agentMap.put("sessionId", ...) at line 232 — IS emitted stateful → agentMap.put("stateful", true) at line 466 — IS emitted 4 callbacks → emitted in agentConfig.callbacks list as task entries Only framework (dispatch key) and frameworkConfig (merged/spread) are genuinely not emitted under their own keys in agentConfig. Renamed section to 'Serialization notes for potentially surprising fields' and corrected each row to reflect what actually happens. Also fixed the two stale rows in the main mapping table (stateful, sessionId). Answer to the user question: this does NOT create bugs. All fields are intentionally handled. The previous section title was misleading and most of its content was factually incorrect. Co-Authored-By: Claude Sonnet 4.6 (1M context) * docs(java-sdk): 3-pass accuracy review — fix all API mismatches vs source Verified every code claim across all 17 docs against actual source. Fixes: Tool registration (4 files): @Tool POJOs use ToolRegistry.fromInstance(obj), NOT AgentTool.from(obj). AgentTool.from takes an Agent (sub-agent → tool). Fixed getting-started, agents, tools, langchain4j + api-reference. Guardrails (agents, guardrails, google-adk): GuardrailDef.regex()/llm()/of() static factories do NOT exist. Rewrote to the real RegexGuardrail.builder() / LLMGuardrail.builder() (return GuardrailDef) and GuardrailDef.builder().func() for custom. OnFail values corrected: RAISE/RETRY/FIX/HUMAN (not BLOCK/WARN). Import corrected: ai.model.GuardrailDef (not ai.guardrail.GuardrailDef). Credentials: Credentials.get(name) takes no ToolContext arg (4 sites). CliConfig: package is ai.execution (not ai.tools); builder has no .command() — only enabled/allowedCommands/timeout/workingDir/allowShell. Scheduling: runNow(ScheduleInfo) not runNow(String); previewNext(cron, n) not nextNExecutions(wireName, n). Fixed scheduling, api-reference, agent-runtime-api. Return types: plan() → CompileResponse (not Map); AgentResult.getOutput() → Object (+ getOutput(Class)); removed non-existent getRawResult(); AgentEvent.getResult() → Object. Removed non-existent no-arg reject(); added isWaiting()/respond(Map). Strategy enum: added ROUND_ROBIN, RANDOM (9 values total). PLAN_EXECUTE: Op.generate takes a Generate object, not boolean — rewrote the multi-agent example to the real args + Ref("stepId") wiring pattern. Defaults: timeoutSeconds default is 0 (server applies its own), not 600. Links: self-hosting link depth corrected (../ not ../../) for the integrated site. Final sweep confirms: 0 residual defect patterns; all doc imports resolve to real source files; all framework bridge signatures match. Co-Authored-By: Claude Sonnet 4.6 (1M context) * feat(java-sdk): add memory, reasoningEffort, maskedFields, contextWindowBudget to Agent Closes the four field gaps documented in agent-structure.md — Python and the server had these; Java Agent now does too. New on Agent.builder(): .memory(ConversationMemory) → "memory": {messages, maxMessages} (server MemoryConfig) .reasoningEffort(String) → "reasoningEffort" ("low"|"medium"|"high") .maskedFields(String.../List) → "maskedFields" (redacted in history/UI) .contextWindowBudget(int) → "contextWindowBudget" (proactive condensation) New class model/ConversationMemory.java — messages + maxMessages, with addUser/addAssistant/addSystem chaining helpers (mirrors Python ConversationMemory). AgentConfigSerializer emits all four with null/empty guards; JSON keys verified against server AgentConfig field names and Python config_serializer output. Tests (SerializerTest, +2): parity_fields_serialized (proven to fail without the serialization) and parity_fields_absent_when_unset. 206 unit tests, 0 failures. docs: agent-structure.md moves these into the main mapping table; the "Python but not Java" section now lists only `dependencies`. api-reference.md Agent.Builder gains the four methods. Co-Authored-By: Claude Sonnet 4.6 (1M context) * test(java-sdk/e2e): add round-trip e2e for memory/reasoningEffort/maskedFields/contextWindowBudget Adds 4 structural plan() tests to Suite17NewParity (Order 19-22), matching the suite's existing no-LLM pattern: build agent with the field set → runtime.plan() (real /agent/compile) → assert the field survives the SDK → server → compiled-output round-trip. - test_reasoning_effort_serialized → agentDef.reasoningEffort == "high" - test_context_window_budget_serialized → agentDef.contextWindowBudget == 8000 - test_masked_fields_serialized → maskedFields (agentDef OR WorkflowDef — server maps it to WorkflowDef.maskedFields, so valueFromPlan() checks both) - test_memory_serialized → agentDef.memory.{maxMessages, messages} No LLM (CLAUDE.md). Compiles against real APIs. Requires a live server to run; the exact echo location per field is confirmed on first run (make-fail validation). Co-Authored-By: Claude Sonnet 4.6 (1M context) * refactor(java-sdk/e2e): rename Suite17NewParity → Suite17ConfigSerialization "NewParity" described when the tests were added, not what they verify. The suite asserts that agent config fields/features round-trip into the compiled agentDef via plan(). Renamed file + class accordingly and fixed the stale "Suite 11" javadoc. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(java-sdk): trim agent-structure.md to a clean field reference Drop the "Cross-Layer Proof" framing, the defensive "all fields serialize correctly / none are missing" reassurances, and the cross-SDK parity-proof sections. Keep the factual reference: fields, builder methods, JSON keys, serialization behavior, and defaults. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(java-sdk): add agent-schema.json wire contract + verified proof Canonical JSON Schema (Draft 2020-12) for the agentConfig that SDKs serialize and POST to the server. Reconciled from the server AgentConfig model (the deserialization target) and both SDK serializers, verified in 3 rounds: 1. Static inventory of server model + nested configs + both SDK emit sets. 2. Direct-source verification of cross-SDK discrepancies — corrected the onFail enum (retry|raise|fix|human), strategy nullability, sessionId/ planSource channel divergences, reasoningEffort value set. 3. Empirical: maximal agent serialized by BOTH SDKs validates against the schema; 6 negative mutations all rejected (schema has teeth); the Java-emitted, schema-valid config compiles on a live server (HTTP 200). agent-schema.md documents the schema and carries the formal proof (soundness/completeness/type-consistency) and the known cross-SDK divergences. Co-Authored-By: Claude Opus 4.8 (1M context) * test(java-sdk/e2e): fix ScheduleIntegrationTest silently skipping under standard env The suite appends its own "/api" to AGENTSPAN_SERVER_URL, but that env var conventionally INCLUDES "/api" (per BaseTest and CI). The mismatch produced a double "/api", so the @EnabledIf scheduler probe 404'd and all 10 tests skipped under the normal convention — i.e. they never ran. Normalize the base URL by stripping a trailing "/api" so the suite runs under either form. Verified: with the standard /api env the suite went from 10 skipped → 10 run, 0 failures; full e2e is now 346 tests, 0 skipped, 0 failures. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(java-sdk): reverse-verify agent-schema via generated dataclass + record generate.py reverse-engineers agent-schema.json into a Python dataclass and a Java record, then proves correctness by diffing the generated models against the server AgentConfig models and validating a generated instance: (a) generated fields ≡ schema (root + 16 nested $defs) (b) generated instance validates against the schema (c) every server field — root + all 13 nested models — is in the schema (0 gaps) The reverse pass confirms nothing was missed. Only surfaced one undocumented SDK-only extra, cliConfig.workingDir (Java emits it; server CliConfig ignores it), now noted in the divergences alongside the tool retry fields. Co-Authored-By: Claude Opus 4.8 (1M context) * refactor(java-sdk): remove Agentspan static facade; AgentRuntime is the entry point The Agentspan static facade wrapped a process-global singleton AgentRuntime with a JVM shutdown hook — a crutch that hurt testability/DI and was the odd one out (AgentRuntime is already AutoCloseable, and the Spring module auto-configures an agentRuntime bean). Users now either construct an AgentRuntime (try-with-resources) or inject the Spring bean. - Move the native-framework drop-in overloads (run/start/stream/deploy/serve/plan/ resume accepting a raw ADK BaseAgent, LangChain4j ChatModel, or LangGraph4j AgentExecutor.Builder) + the coerceAgent/reflection helpers ONTO AgentRuntime as instance methods, so the runtime is now the complete API. No capability lost. - Delete Agentspan.java. - Rewrite 147 examples to construct a main-local AgentRuntime and call runtime.*. - Update README, framework-bridge Javadoc, and docs to AgentRuntime. Builds: core + examples + spring compile; unit tests 206/0/0. AgentRuntime change is purely additive, and e2e suites already use AgentRuntime directly (unaffected). Co-Authored-By: Claude Opus 4.8 (1M context) * refactor(java-sdk): remove UserProxyAgent — non-functional parity stub UserProxyAgent.create(...) only stamped inert metadata (_agent_type=user_proxy, _human_input_mode, _default_response) that NOTHING consumes — the server has no user-proxy/human-input-mode handling, so the documented "pauses with a HumanTask and waits for real human input" behaviour never happened. It produced a plain LLM agent with misleading metadata, was used only by its own tests (no examples, no production path), and its class Javadoc example referenced a 1-arg create(String) overload that didn't even exist. Real HITL is HumanTool / WaitForMessageTool / MANUAL strategy. Deletes UserProxyAgent.java + its 2 unit tests (SerializerTest) + 1 e2e test (Suite17ConfigSerialization) and the stale references. Builds green; full unit+e2e 343/0/0 against a live server. Co-Authored-By: Claude Opus 4.8 (1M context) * refactor: remove UserProxyAgent from Python, TypeScript, and C# SDKs Cross-SDK parity with the Java removal: UserProxyAgent only stamped inert _agent_type=user_proxy / _human_input_mode / _default_response metadata that the server never interprets, so the documented "pauses for human input" behaviour never happened. Real HITL is human_tool / wait-for-message / MANUAL strategy. Removed in each SDK: the class, its export, its dedicated example (27_*), its kitchen-sink usage, and its tests — keeping GPTAssistantAgent intact. Also updated current API docs (docs/python-sdk/*), the SDK READMEs/CHANGELOG, the python validation group, top-level README, and AGENTS.md. Left untouched: historical design records under docs/sdk-design/ and docs/design/ (dated artifacts; flagged separately). Verified: Python 1653 unit tests pass; TypeScript builds + 822 unit tests pass; C# builds + 161 tests pass. Repo grep clean outside the design docs. Co-Authored-By: Claude Opus 4.8 (1M context) * docs: scrub UserProxyAgent from design records (sdk-design + design plans/specs) Completes the cross-SDK removal: the design docs no longer reference UserProxyAgent now that it's gone from all SDKs. Removed its sections, feature-matrix/example rows, source-tree listing mentions, prose list items, and the editorial_reviewer kitchen-sink participant across the multi-language design, TS/Java/Go/Ruby/Kotlin docs, and the 2026-03 plans/specs. GPTAssistantAgent and all other content kept intact; historical feature/section numbering left as-is. Repo-wide grep for UserProxyAgent/user_proxy is now zero (outside build artifacts). Co-Authored-By: Claude Opus 4.8 (1M context) * refactor * refactor(java-sdk): unify credential access onto ToolContext; drop static Credentials Tool secrets were read via a public static thread-local accessor (Credentials.get), inconsistent with every other per-call value (ids, shared state) which arrives on the injected ToolContext — and it leaked framework-only setForCall/clearForCall onto a public class. Now a tool reads ctx.getCredential(name) / getCredentialOrNull(name) on its ToolContext, one injected object, no public framework hooks. - ToolContext gains an IMMUTABLE per-call credential snapshot + getCredential/ getCredentialOrNull/getCredentials. Multi-threading: because the snapshot lives on the context object (not a thread-local), a thread the tool spawns can read it, and it stays valid after the worker thread clears the transport — the old static silently failed off-thread. - New internal CredentialContext (thread-local) carries resolved secrets from WorkerManager → ToolRegistry on the worker thread (secrets never enter task input/output). - Deleted the public Credentials class; updated Example16, e2e Suite2, WorkerCredentialFetcher javadoc; replaced CredentialsTest with ToolContextCredentialsTest. Verified: full unit+e2e 344/0/0 (incl. Suite2 credential round-trip on a live server + secret store). Make-fail: dropping the snapshot fails 4 unit tests incl. the multi-thread guarantee, confirming the suite has teeth. Co-Authored-By: Claude Opus 4.8 (1M context) * refactor(java-sdk): remove ClaudeCode — non-functional stub in Java The Java ClaudeCode class advertised a PermissionMode it silently dropped: toModelString() encodes only "claude-code/{model}", the serializer never emits permissionMode, and the server never reads it — so new ClaudeCode("opus", BYPASS) had zero effect. Worse, Java has no client-side claude-agent-sdk worker (Python runs claude-code agents via a local claude_agent_sdk worker and emits a passthrough stub; Java emits a plain native agent and has no worker to execute it), so the class couldn't deliver a working feature at all. Used only by tests; no examples. Deletes ClaudeCode.java + its 3 unit tests (SerializerTest) + 2 e2e tests (Suite17ConfigSerialization) and the import/javadoc references. Targeting a claude-code-capable server is still possible via a raw model string if ever wired. Python keeps its ClaudeCode — there permission_mode IS consumed by the claude_agent_sdk runtime worker, so it's functional and stays. Compiles (core + e2e + examples); unit tests pass, 0 failures. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(java-sdk): drop ClaudeCode test references (completes prior commit) The previous commit deleted ClaudeCode.java but a failed `git add` left these two test files out, so that revision didn't compile. This removes the ClaudeCode unit tests (SerializerTest) and e2e tests + import/javadoc (Suite17ConfigSerialization) that referenced the deleted class. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(java-sdk): keep optional framework types out of AgentRuntime signatures; fix spring prop prefix CI java-sdk-tests failed: :spring:test threw NoClassDefFoundError for org.bsc.langgraph4j...AgentExecutor$Builder. Root cause: moving the TYPED LangChain4j/LangGraph4j drop-in overloads onto AgentRuntime put compileOnly framework types into the core class's method signatures. Spring introspects the AgentRuntime bean's methods (resolving every parameter type), which force-loads those optional types — absent from the spring classpath. The old Agentspan facade had the same overloads but Spring never loaded it, so the issue was latent. Fix: AgentRuntime now exposes only Object-typed drop-ins. The native LangChain4j ChatModel / LangGraph4j AgentExecutor.Builder are detected reflectively in coerceAgent (by FQN, same pattern as ADK BaseAgent) and built in method BODIES, so no compileOnly type appears in any signature — Spring introspection is safe. Added run/runAsync/start/stream(Object, String, Object... tools) for the tool-POJO form; fixed-arity overloads still win resolution (internal null calls cast to Plan). Examples calling runtime.run(model|builder, prompt[, tools]) are unchanged. Also fixes a separate spring-rename mismatch this unmasked: AgentProperties was @ConfigurationProperties(prefix="conductor.agent") while its tests and own Javadoc use the agentspan.* prefix — restored prefix to "agentspan". Verified: :test + :spring:test (5/0/0) green; examples + e2e compile. Co-Authored-By: Claude Opus 4.8 (1M context) * agentdef * Support Agentspan Embedded in Orkes Conductor (#273) * Spotless * Spotless * fix(python-sdk): update http client tests for X-Authorization auth rework PR #273 replaced the sync _base_headers() (x-auth-key headers / Bearer auth) with the async _auth_headers() flow — api_key sent directly as X-Authorization, auth_key/secret minting a cached JWT via POST /token — but left the unit tests asserting the old contract, breaking CI. Rewrite the tests against the new contract: anonymous requests carry no auth header, api_key is sent without minting and wins over auth_key, auth_key/secret mint once and cache (verified by mutation), and a failing /token endpoint degrades to an unauthenticated request. * feat(java-sdk): harden @AgentDef discovery; add Spring AgentCatalog Discovery now walks the full type hierarchy (superclasses + interfaces) instead of getMethods(), fixing three silent failure modes: - non-public @AgentDef methods now throw instead of vanishing - an unannotated override (e.g. a CGLIB proxy of a @Transactional bean) no longer hides the agent — the nearest annotated declaration wins and invocation still dispatches to the override - @AgentDef combined with @Tool/@GuardrailDef on one method is rejected Spring module gains an auto-configured AgentCatalog that collects @AgentDef agents from every bean: lazy scan on first access, only agent-declaring beans instantiated, duplicate names fail fast naming both beans. Also replaces leftover inline fully-qualified names with imports. --------- Co-authored-by: Dale Brady <49766562+bradyyie@users.noreply.github.com> Co-authored-by: bradyyie --- .github/workflows/ci-csharp-sdk-e2e.yml | 2 +- .github/workflows/ci-java-sdk-e2e.yml | 2 +- .github/workflows/ci.yml | 20 +- .github/workflows/release-server-maven.yml | 50 + AGENTS.md | 2 +- README.md | 1 - cli/auth/auth0.go | 227 ++++ cli/auth/auth0_test.go | 181 +++ cli/auth/orkes.go | 60 + cli/auth/orkes_test.go | 62 + cli/auth/pkce.go | 174 +++ cli/auth/pkce_test.go | 159 +++ cli/client/client.go | 98 +- cli/cmd/credentials.go | 7 +- cli/cmd/helpers.go | 10 + cli/cmd/login.go | 309 ++++- cli/cmd/login_test.go | 143 ++- cli/cmd/root.go | 2 +- cli/config/config.go | 26 + cli/config/token.go | 85 ++ ...6-03-23-multi-language-sdk-deliverables.md | 14 +- ...026-03-24-typescript-sdk-implementation.md | 4 +- .../specs/2026-03-23-typescript-sdk-design.md | 22 +- docs/java-sdk | 1 + docs/python-sdk/agent-configuration.md | 27 - docs/python-sdk/api-reference.md | 1 - docs/python-sdk/compilation-comparison.md | 1 - .../2026-03-23-multi-language-sdk-design.md | 13 +- docs/sdk-design/go.md | 9 +- docs/sdk-design/java.md | 2 +- docs/sdk-design/kitchen-sink.md | 2 - docs/sdk-design/kotlin.md | 10 +- docs/sdk-design/ruby.md | 9 - docs/sdk-design/typescript.md | 12 +- mkdocs.yml | 22 + sdk/csharp/CHANGELOG.md | 2 +- .../Example27UserProxyAgent.csproj | 12 - .../examples/27_UserProxyAgent/Program.cs | 87 -- sdk/csharp/src/Agentspan/Tool.cs | 59 +- sdk/csharp/src/Agentspan/UserProxyAgent.cs | 61 - .../Agentspan.OpenAI.Tests/CliToolTests.cs | 66 + sdk/java/README.md | 38 +- sdk/java/build.gradle | 59 +- sdk/java/docs/agent-client-api.md | 339 ++++++ sdk/java/docs/agent-runtime-api.md | 384 ++++++ sdk/java/docs/agent-schema.json | 317 +++++ sdk/java/docs/agent-schema.md | 163 +++ sdk/java/docs/agent-structure.md | 88 ++ sdk/java/docs/api-reference.md | 406 +++++++ sdk/java/docs/concepts/agents.md | 302 +++++ sdk/java/docs/concepts/guardrails.md | 131 ++ sdk/java/docs/concepts/multi-agent.md | 226 ++++ sdk/java/docs/concepts/scheduling.md | 88 ++ sdk/java/docs/concepts/skills.md | 97 ++ sdk/java/docs/concepts/termination.md | 80 ++ sdk/java/docs/concepts/tools.md | 234 ++++ sdk/java/docs/frameworks/google-adk.md | 82 ++ sdk/java/docs/frameworks/langchain4j.md | 83 ++ sdk/java/docs/frameworks/openai.md | 91 ++ sdk/java/docs/generated/AgentConfigModel.java | 25 + sdk/java/docs/generated/agent_config.py | 190 +++ sdk/java/docs/generated/generate.py | 127 ++ sdk/java/docs/getting-started.md | 136 +++ sdk/java/docs/index.md | 59 + sdk/java/docs/mkdocs.yml | 59 + sdk/java/docs/spring-boot.md | 144 +++ sdk/java/e2e/BaseTest.java | 56 +- sdk/java/e2e/PlanExecuteTest.java | 1062 +++++++++-------- sdk/java/e2e/Suite10CodeExecution.java | 522 ++++---- sdk/java/e2e/Suite11LangChain4j.java | 195 +-- sdk/java/e2e/Suite11bOpenAIAgent.java | 216 ++++ sdk/java/e2e/Suite12HandoffApprove.java | 130 +- sdk/java/e2e/Suite12TerminationGates.java | 110 +- sdk/java/e2e/Suite13Callbacks.java | 278 +++-- sdk/java/e2e/Suite14StatefulDomain.java | 422 +++---- sdk/java/e2e/Suite15Skills.java | 569 +++++---- sdk/java/e2e/Suite16Synthesize.java | 64 +- sdk/java/e2e/Suite17ConfigSerialization.java | 740 ++++++++++++ sdk/java/e2e/Suite17NewParity.java | 690 ----------- sdk/java/e2e/Suite18ToolTypes.java | 87 +- sdk/java/e2e/Suite19ManualStrategy.java | 177 +++ sdk/java/e2e/Suite1BasicValidation.java | 412 ++++--- sdk/java/e2e/Suite2ToolCalling.java | 48 +- .../e2e/Suite2ToolCallingCredentials.java | 107 +- sdk/java/e2e/Suite3CliTools.java | 463 ++++--- sdk/java/e2e/Suite4McpTools.java | 310 ++--- sdk/java/e2e/Suite5HttpTools.java | 306 ++--- sdk/java/e2e/Suite6PdfTools.java | 350 +++--- sdk/java/e2e/Suite7MediaTools.java | 351 +++--- sdk/java/e2e/Suite8Guardrails.java | 92 +- sdk/java/e2e/Suite8bGuardrailsExtended.java | 338 +++--- sdk/java/e2e/Suite9Handoffs.java | 394 +++--- sdk/java/e2e/SuiteHttpApi404.java | 52 +- sdk/java/examples/VERIFICATION.md | 6 +- sdk/java/examples/build.gradle | 10 +- .../ai}/examples/Example01BasicAgent.java | 13 +- .../ai}/examples/Example02Tools.java | 19 +- .../ai}/examples/Example02aSimpleTools.java | 19 +- .../examples/Example02bMultiStepTools.java | 19 +- .../ai}/examples/Example02cTypedToolArgs.java | 19 +- .../examples/Example03StructuredOutput.java | 19 +- .../examples/Example04HttpAndMcpTools.java | 23 +- .../ai}/examples/Example05Handoffs.java | 21 +- .../examples/Example06SequentialPipeline.java | 13 +- .../ai}/examples/Example07ParallelAgents.java | 15 +- .../ai}/examples/Example08RouterAgent.java | 17 +- .../ai}/examples/Example09HumanInTheLoop.java | 18 +- .../Example09bHandoffHumanInTheLoop.java | 20 +- .../examples/Example108PlanExecuteRefs.java | 28 +- .../ai}/examples/Example10Guardrails.java | 29 +- .../examples/Example115PlannerContext.java | 16 +- .../ai}/examples/Example11Streaming.java | 22 +- .../ai}/examples/Example12LongRunning.java | 10 +- .../examples/Example13HierarchicalAgents.java | 17 +- .../examples/Example14ExistingWorkers.java | 19 +- .../examples/Example15AgentDiscussion.java | 15 +- .../examples/Example16CredentialsTool.java | 40 +- .../ai}/examples/Example16RandomStrategy.java | 15 +- .../examples/Example17SwarmOrchestration.java | 19 +- .../examples/Example18ManualSelection.java | 17 +- .../Example19ComposableTermination.java | 31 +- .../Example20ConstrainedTransitions.java | 15 +- .../examples/Example21RegexGuardrails.java | 25 +- .../ai}/examples/Example22LlmGuardrails.java | 19 +- .../ai}/examples/Example23TokenTracking.java | 21 +- .../examples/Example29AgentIntroductions.java | 15 +- .../examples/Example31ToolInputGuardrail.java | 29 +- .../ai}/examples/Example32HumanGuardrail.java | 29 +- .../examples/Example33ExternalWorkers.java | 19 +- .../ai}/examples/Example33SingleTurnTool.java | 19 +- .../examples/Example34PromptTemplates.java | 21 +- .../Example35StandaloneGuardrails.java | 7 +- .../Example36SimpleAgentGuardrails.java | 21 +- .../ai}/examples/Example37FixGuardrail.java | 27 +- .../ai}/examples/Example38TechTrends.java | 21 +- .../Example41SequentialPipelineTools.java | 21 +- .../examples/Example42SecurityTesting.java | 21 +- .../Example43DataSecurityPipeline.java | 21 +- .../examples/Example44SafetyGuardrails.java | 21 +- .../ai}/examples/Example45AgentTool.java | 21 +- .../examples/Example46TransferControl.java | 21 +- .../ai}/examples/Example47Callbacks.java | 19 +- .../ai}/examples/Example48Planner.java | 19 +- .../examples/Example49IncludeContents.java | 23 +- .../ai}/examples/Example50ThinkingConfig.java | 19 +- .../ai}/examples/Example51SharedState.java | 21 +- .../examples/Example52NestedStrategies.java | 15 +- .../Example53AgentLifecycleCallbacks.java | 21 +- .../Example54SoftwareBugAssistant.java | 23 +- .../Example55MlEngineeringPipeline.java | 15 +- .../ai}/examples/Example56RagAgent.java | 19 +- .../ai}/examples/Example57PlanDryRun.java | 19 +- .../ai}/examples/Example58ScatterGather.java | 22 +- .../ai}/examples/Example59CodingAgent.java | 15 +- .../ai}/examples/Example64SwarmWithTools.java | 25 +- .../examples/Example65ParallelWithTools.java | 21 +- .../examples/Example66HandoffToParallel.java | 17 +- .../examples/Example67RouterToSequential.java | 17 +- .../Example68ContextCondensation.java | 21 +- .../ai}/examples/Example69Skills.java | 21 +- .../ai/examples/Example70AnnotatedAgent.java | 47 + .../ai}/examples/Example99ScheduledAgent.java | 12 +- .../conductor/ai}/examples/Settings.java | 2 +- .../ai}/examples/VerifyHandoffs.java | 14 +- .../conductor/ai}/examples/VerifyRouting.java | 14 +- .../ai}/examples/adk/Example00HelloWorld.java | 15 +- .../ai}/examples/adk/Example01BasicAgent.java | 15 +- .../examples/adk/Example02FunctionTools.java | 15 +- .../adk/Example03StructuredOutput.java | 13 +- .../ai}/examples/adk/Example04SubAgents.java | 13 +- .../adk/Example05GenerationConfig.java | 15 +- .../ai}/examples/adk/Example06Streaming.java | 15 +- .../examples/adk/Example07OutputKeyState.java | 13 +- .../adk/Example08InstructionTemplating.java | 13 +- .../examples/adk/Example09MultiToolAgent.java | 13 +- .../adk/Example10HierarchicalAgents.java | 13 +- .../adk/Example11SequentialAgent.java | 13 +- .../examples/adk/Example12ParallelAgent.java | 13 +- .../ai}/examples/adk/Example13LoopAgent.java | 13 +- .../ai}/examples/adk/Example14Callbacks.java | 13 +- .../adk/Example15GlobalInstruction.java | 13 +- .../adk/Example16CustomerService.java | 13 +- .../adk/Example17FinancialAdvisor.java | 13 +- .../adk/Example18OrderProcessing.java | 13 +- .../examples/adk/Example19SupplyChain.java | 13 +- .../ai}/examples/adk/Example20BlogWriter.java | 13 +- .../ai}/examples/adk/Example21AgentTool.java | 13 +- .../adk/Example22TransferControl.java | 13 +- .../ai}/examples/adk/Example23Callbacks.java | 13 +- .../ai}/examples/adk/Example24Planner.java | 13 +- .../examples/adk/Example25CamelSecurity.java | 15 +- .../adk/Example26SafetyGuardrails.java | 13 +- .../examples/adk/Example27SecurityAgent.java | 13 +- .../examples/adk/Example28MoviePipeline.java | 13 +- .../adk/Example29IncludeContents.java | 13 +- .../examples/adk/Example30ThinkingConfig.java | 13 +- .../examples/adk/Example31SharedState.java | 13 +- .../adk/Example32NestedStrategies.java | 13 +- .../adk/Example33SoftwareBugAssistant.java | 13 +- .../examples/adk/Example34MlEngineering.java | 15 +- .../ai}/examples/adk/Example35RagAgent.java | 15 +- .../examples/adk/Example36BuiltInTools.java | 13 +- .../examples/adk/Example37DeployAndServe.java | 25 +- .../adk/Example38AgentspanGuardrails.java | 25 +- .../langchain/Example01HelloWorld.java | 11 +- .../langchain/Example02ReactWithTools.java | 11 +- .../langchain/Example03CustomTools.java | 11 +- .../langchain/Example04StructuredOutput.java | 11 +- .../langchain/Example05PromptTemplates.java | 11 +- .../langchain/Example06ChatHistory.java | 15 +- .../langchain/Example07MemoryAgent.java | 15 +- .../langchain/Example08MultiToolAgent.java | 11 +- .../langchain/Example09MathCalculator.java | 11 +- .../langchain/Example10WebSearchAgent.java | 11 +- .../langchain/Example11CodeReviewAgent.java | 11 +- .../Example12DocumentSummarizer.java | 11 +- .../Example13CustomerServiceAgent.java | 11 +- .../langchain/Example14ResearchAssistant.java | 11 +- .../langchain/Example15DataAnalyst.java | 11 +- .../langchain/Example16ContentWriter.java | 11 +- .../examples/langchain/Example17SqlAgent.java | 11 +- .../langchain/Example18EmailDrafter.java | 11 +- .../langchain/Example19FactChecker.java | 11 +- .../langchain/Example20TranslationAgent.java | 11 +- .../langchain/Example21SentimentAnalysis.java | 11 +- .../Example22ClassificationAgent.java | 11 +- .../Example23RecommendationAgent.java | 11 +- .../langchain/Example24OutputParsers.java | 11 +- .../Example25AdvancedOrchestration.java | 11 +- .../Example26AgentspanGuardrails.java | 23 +- .../langchain/ExampleCredentials.java | 21 +- .../examples/langchain/ExamplePipeline.java | 15 +- .../langgraph/Example01HelloWorld.java | 11 +- .../langgraph/Example02ReactWithTools.java | 11 +- .../examples/langgraph/Example03Memory.java | 15 +- .../langgraph/Example04SimpleStateGraph.java | 11 +- .../examples/langgraph/Example05ToolNode.java | 11 +- .../Example06ConditionalRouting.java | 11 +- .../langgraph/Example07SystemPrompt.java | 11 +- .../langgraph/Example08StructuredOutput.java | 11 +- .../langgraph/Example09MathAgent.java | 11 +- .../langgraph/Example10ResearchAgent.java | 11 +- .../langgraph/Example11CustomerSupport.java | 11 +- .../examples/openai/Example01BasicAgent.java | 17 +- .../openai/Example02FunctionTools.java | 21 +- .../openai/Example03StructuredOutput.java | 17 +- .../examples/openai/Example04Handoffs.java | 19 +- .../examples/openai/Example05Guardrails.java | 19 +- .../openai/Example06ModelSettings.java | 19 +- .../examples/openai/Example07Streaming.java | 19 +- .../examples/openai/Example08AgentAsTool.java | 19 +- .../openai/Example09DynamicInstructions.java | 19 +- .../examples/openai/Example10MultiModel.java | 19 +- sdk/java/spring/build.gradle | 10 +- .../spring/AgentspanAutoConfiguration.java | 31 - .../agentspan/spring/AgentspanProperties.java | 28 - .../ai/spring/AgentAutoConfiguration.java | 56 + .../conductor/ai/spring/AgentCatalog.java | 130 ++ .../conductor/ai/spring/AgentProperties.java | 47 + ...ot.autoconfigure.AutoConfiguration.imports | 2 +- .../AgentspanAutoConfigurationTest.java | 75 -- .../ai/spring/AgentAutoConfigurationTest.java | 85 ++ .../conductor/ai/spring/AgentCatalogTest.java | 107 ++ .../main/java/ai/agentspan/AgentConfig.java | 91 -- .../src/main/java/ai/agentspan/Agentspan.java | 546 --------- .../main/java/ai/agentspan/ClaudeCode.java | 55 - .../main/java/ai/agentspan/Credentials.java | 114 -- .../java/ai/agentspan/UserProxyAgent.java | 66 - .../ai/agentspan/execution/CliConfig.java | 65 - .../java/ai/agentspan/internal/HttpApi.java | 176 --- .../java/ai/agentspan/internal/SseClient.java | 182 --- .../internal/WorkerCredentialFetcher.java | 111 -- .../ai/agentspan/internal/WorkerHttp.java | 128 -- .../ai/agentspan/internal/WorkerManager.java | 343 ------ .../java/ai/agentspan/model/ToolContext.java | 43 - .../ai/agentspan/schedule/ScheduleInfo.java | 74 -- .../conductoross/conductor/ai}/Agent.java | 478 ++++++-- .../conductor/ai/AgentConfig.java | 67 ++ .../conductor/ai}/AgentRuntime.java | 722 +++++++---- .../conductor/ai}/CallbackHandler.java | 26 +- .../conductor/ai/annotations/AgentDef.java | 133 +++ .../ai}/annotations/GuardrailDef.java | 10 +- .../conductor/ai}/annotations/Tool.java | 4 +- .../conductor/ai}/enums/AgentStatus.java | 2 +- .../conductor/ai}/enums/EventType.java | 8 +- .../conductor/ai/enums/Framework.java | 68 ++ .../conductor/ai}/enums/OnFail.java | 8 +- .../conductor/ai}/enums/Position.java | 8 +- .../conductor/ai}/enums/Strategy.java | 8 +- .../ai}/exceptions/AgentAPIException.java | 2 +- .../exceptions/AgentNotFoundException.java | 2 +- .../ai}/exceptions/AgentspanException.java | 2 +- .../exceptions/CredentialAuthException.java | 2 +- .../CredentialNotFoundException.java | 2 +- .../CredentialRateLimitException.java | 2 +- .../CredentialServiceException.java | 2 +- .../ai/execution/CliCommandExecutor.java | 284 +++++ .../conductor/ai/execution/CliConfig.java | 105 ++ .../conductor/ai}/execution/CodeExecutor.java | 39 +- .../ai}/execution/DockerCodeExecutor.java | 33 +- .../ai}/execution/ExecutionResult.java | 22 +- .../conductor/ai}/frameworks/AdkBridge.java | 210 ++-- .../ai}/frameworks/LangChain4jAgent.java | 64 +- .../ai}/frameworks/LangChainBridge.java | 25 +- .../conductor/ai}/frameworks/OpenAIAgent.java | 93 +- .../conductor/ai}/gate/TextGate.java | 2 +- .../conductor/ai}/guardrail/Guardrail.java | 29 +- .../conductor/ai}/guardrail/LLMGuardrail.java | 45 +- .../ai}/guardrail/RegexGuardrail.java | 45 +- .../conductor/ai}/handoff/Handoff.java | 2 +- .../conductor/ai}/handoff/OnCondition.java | 2 +- .../conductor/ai}/handoff/OnTextMention.java | 2 +- .../conductor/ai}/handoff/OnToolResult.java | 11 +- .../conductor/ai/internal/AgentClient.java | 115 ++ .../ai}/internal/AgentConfigSerializer.java | 225 +++- .../conductor/ai/internal/AgentRegistry.java | 350 ++++++ .../conductor/ai/internal/AgentRequest.java | 202 ++++ .../ai/internal/AgentStatusResponse.java | 99 ++ .../ai/internal/CredentialContext.java | 45 + .../conductor/ai}/internal/JsonMapper.java | 4 +- .../conductor/ai/internal/PendingTool.java | 67 ++ .../conductor/ai/internal/RespondBody.java | 93 ++ .../conductor/ai/internal/SseClient.java | 173 +++ .../conductor/ai/internal/StartResponse.java | 58 + .../conductor/ai}/internal/ToolRegistry.java | 101 +- .../ai/internal/WorkerCredentialFetcher.java | 95 ++ .../conductor/ai/internal/WorkerManager.java | 440 +++++++ .../conductor/ai}/model/AgentEvent.java | 74 +- .../conductor/ai}/model/AgentHandle.java | 218 ++-- .../conductor/ai}/model/AgentResult.java | 45 +- .../conductor/ai}/model/AgentStream.java | 85 +- .../conductor/ai/model/CompileResponse.java | 47 + .../ai/model/ConversationMemory.java | 86 ++ .../conductor/ai}/model/CredentialFile.java | 14 +- .../conductor/ai}/model/DeploymentInfo.java | 10 +- .../conductor/ai}/model/GuardrailDef.java | 85 +- .../conductor/ai}/model/GuardrailResult.java | 2 +- .../conductor/ai}/model/PrefillToolCall.java | 11 +- .../conductor/ai}/model/PromptTemplate.java | 16 +- .../conductor/ai}/model/TokenUsage.java | 6 +- .../conductor/ai/model/ToolContext.java | 114 ++ .../conductor/ai}/model/ToolDef.java | 190 ++- .../ai}/openai/GPTAssistantAgent.java | 132 +- .../conductor/ai}/plans/Action.java | 2 +- .../conductor/ai}/plans/Context.java | 2 +- .../conductor/ai}/plans/Generate.java | 2 +- .../conductoross/conductor/ai}/plans/Op.java | 5 +- .../conductor/ai}/plans/Plan.java | 19 +- .../conductor/ai}/plans/PlanValues.java | 2 +- .../conductoross/conductor/ai}/plans/Ref.java | 2 +- .../conductor/ai}/plans/Step.java | 2 +- .../conductor/ai}/plans/Validation.java | 2 +- .../conductor/ai}/schedule/Schedule.java | 108 +- .../ai}/schedule/ScheduleException.java | 14 +- .../conductor/ai/schedule/ScheduleInfo.java | 137 +++ .../conductor/ai}/schedule/Schedules.java | 219 ++-- .../conductor/ai}/skill/Skill.java | 148 +-- .../conductor/ai}/skill/SkillLoadError.java | 2 +- .../ai}/termination/AndTermination.java | 11 +- .../termination/MaxMessageTermination.java | 2 +- .../ai}/termination/OrTermination.java | 11 +- .../termination/StopMessageTermination.java | 2 +- .../ai}/termination/TerminationCondition.java | 2 +- .../ai}/termination/TerminationResult.java | 11 +- .../termination/TextMentionTermination.java | 2 +- .../termination/TokenUsageTermination.java | 16 +- .../conductor/ai/tools}/AgentTool.java | 50 +- .../conductor/ai}/tools/HttpTool.java | 43 +- .../conductor/ai}/tools/HumanTool.java | 6 +- .../conductor/ai}/tools/McpTool.java | 43 +- .../conductor/ai}/tools/MediaTools.java | 38 +- .../conductor/ai}/tools/PdfTool.java | 10 +- .../conductor/ai/tools}/RagTools.java | 55 +- .../ai}/tools/WaitForMessageTool.java | 6 +- .../java/ai/agentspan/CredentialsTest.java | 138 --- .../conductor/ai/AgentAnnotationTest.java | 610 ++++++++++ .../conductor/ai}/AgentBuilderTest.java | 130 +- .../conductor/ai}/ModelExecutionIdTest.java | 25 +- .../conductor/ai}/SerializerTest.java | 692 ++++++----- .../ai/ToolContextCredentialsTest.java | 157 +++ .../ai/exceptions/ExceptionsTest.java | 57 + .../ai/execution/CliCommandExecutorTest.java | 163 +++ .../ai/execution/CodeExecutorAsToolTest.java | 32 + .../ai/frameworks/AdkBridgeTest.java | 98 ++ .../conductor/ai/handoff/HandoffTest.java | 44 + .../ai}/internal/ToolRegistryTest.java | 92 +- .../ai/internal/WorkerManagerDomainTest.java | 89 ++ .../WorkerManagerThreadCountTest.java | 86 ++ .../ai/internal/WorkerManagerTimeoutTest.java | 44 + .../ai/model/AgentHandleErrorTest.java | 83 ++ .../conductor/ai/model/ModelTest.java | 101 ++ .../conductor/ai}/plans/ContextTest.java | 24 +- .../conductor/ai}/plans/OpTest.java | 43 +- .../conductor/ai/plans/PlansTest.java | 73 ++ .../ai}/schedule/ScheduleIntegrationTest.java | 157 ++- .../conductor/ai}/schedule/ScheduleTest.java | 80 +- .../TerminationConditionsTest.java | 57 + .../conductor/ai/tools/ToolsTest.java | 68 ++ sdk/python/README.md | 1 - sdk/python/examples/27_user_proxy_agent.py | 102 -- sdk/python/examples/README.md | 2 - sdk/python/examples/kitchen_sink.py | 13 +- sdk/python/src/agentspan/agents/__init__.py | 3 +- .../agentspan/agents/_internal/token_utils.py | 100 ++ sdk/python/src/agentspan/agents/cli_config.py | 57 +- sdk/python/src/agentspan/agents/ext.py | 86 +- .../agents/frameworks/claude_agent_sdk.py | 42 +- .../agentspan/agents/frameworks/langchain.py | 7 +- .../agentspan/agents/frameworks/langgraph.py | 7 +- .../agentspan/agents/runtime/http_client.py | 61 +- .../src/agentspan/agents/runtime/runtime.py | 92 +- sdk/python/tests/unit/test_cli_config.py | 44 + sdk/python/tests/unit/test_ext.py | 28 +- sdk/python/tests/unit/test_http_client.py | 130 +- sdk/python/tests/unit/test_new_features.py | 57 +- sdk/python/tests/unit/test_sse_client.py | 46 +- sdk/python/tests/unit/test_token_utils.py | 109 ++ sdk/python/validation/groups.py | 1 - .../examples/27-user-proxy-agent.ts | 116 -- sdk/typescript/examples/kitchen-sink.ts | 13 +- sdk/typescript/src/cli-config.ts | 85 +- sdk/typescript/src/ext.ts | 31 - sdk/typescript/src/index.ts | 4 +- sdk/typescript/tests/unit/cli-config.test.ts | 91 ++ .../unit/kitchen-sink-structural.test.ts | 11 +- sdk/typescript/vitest.config.ts | 11 +- server/build.gradle | 292 ++--- .../conductor-agentspan-server/build.gradle | 172 +++ .../dev/agentspan/runtime/AgentRuntime.java | 9 +- .../agentspan/runtime/auth/AuthFilter.java | 47 + .../agentspan/runtime/config/CorsConfig.java | 0 .../runtime/config/ShutdownConfig.java | 0 .../runtime/config/StaticDocsConfig.java | 0 .../runtime/config/UiRoutingConfig.java | 0 .../CredentialDataSourceConfig.java | 0 .../credentials/CredentialEnvSeeder.java | 62 +- .../credentials/CredentialSchemaMigrator.java | 0 .../EncryptedDbCredentialStoreProvider.java | 1 + .../runtime/credentials/MasterKeyConfig.java | 0 .../credentials/NoOpSecretOutputMasker.java | 29 + .../runtime/metrics/MetricsFilterConfig.java | 0 .../ConductorPayloadSkillPackageStore.java | 3 + .../skill/FileSystemSkillMetadataDAO.java | 222 ++++ .../skill/FileSystemSkillPackageStore.java | 3 + .../resources/application-postgres.properties | 0 .../main/resources/application-rag.properties | 0 .../src/main/resources/application.properties | 9 - .../src/main/resources/banner.txt | 0 .../src/main/resources/log4j2.xml | 0 .../resources/schema-credentials-postgres.sql | 18 - .../src/main/resources/schema-credentials.sql | 18 - .../main/resources/static/agentspan-icon.svg | 0 .../resources/static/agentspan-logo-dark.svg | 0 .../resources/static/agentspan-logo-light.svg | 0 .../resources/static/agentspan-logo-small.svg | 0 .../main/resources/static/agentspan-logo.svg | 0 .../static/assets/DailyMotion-DuBKwtXw.js} | 2 +- .../static/assets/Facebook-D-v5cNby.js} | 2 +- .../static/assets/FilePlayer-BGKWD3yK.js} | 2 +- .../static/assets/Kaltura-CeCCDjbU.js} | 2 +- .../static/assets/Mixcloud-adpnMd5P.js} | 2 +- .../resources/static/assets/Mux-8MezvdiC.js} | 2 +- .../static/assets/Preview-BMOGze4w.js} | 2 +- .../static/assets/SoundCloud-_btX2qkW.js} | 2 +- .../static/assets/Streamable-B4sBlscb.js} | 2 +- .../static/assets/Twitch-qhW1EEt2.js} | 2 +- .../static/assets/Vidyard-BJO2x236.js} | 2 +- .../static/assets/Vimeo-C4XIslfc.js} | 2 +- .../static/assets/Wistia-DSoqhXcd.js} | 2 +- .../static/assets/YouTube-CLOSIIy_.js} | 2 +- .../resources/static/assets/abap-DLDM7-KI.js | 0 .../resources/static/assets/apex-DNDY2TF8.js | 0 .../resources/static/assets/azcli-Y6nb8tq_.js | 0 .../resources/static/assets/bat-BwHxbl9M.js | 0 .../resources/static/assets/bicep-CFznDFnq.js | 0 .../static/assets/cameligo-Bf6VGUru.js | 0 .../static/assets/clojure-Dnu-v4kV.js | 0 .../static/assets/codicon-ngg6Pgfi.ttf | Bin .../static/assets/coffee-Bd8akH9Z.js | 0 .../resources/static/assets/cpp-BbWJElDN.js | 0 .../static/assets/csharp-Co3qMtFm.js | 0 .../resources/static/assets/csp-D-4FJmMZ.js | 0 .../resources/static/assets/css-DdJfP1eB.js | 0 .../static/assets/css.worker-DBVD8oXr.js | 0 .../static/assets/cssMode-Bg4Vg_j9.js} | 2 +- .../static/assets/cypher-cTPe9QuQ.js | 0 .../resources/static/assets/dart-BOtBlQCF.js | 0 .../static/assets/dockerfile-BG73LgW2.js | 0 .../resources/static/assets/ecl-BEgZUVRK.js | 0 .../static/assets/elixir-BkW5O-1t.js | 0 .../assets/email-not-verified-C6p1YrlM.svg | 0 .../resources/static/assets/flow9-BeJ5waoc.js | 0 .../static/assets/freemarker2-DiIhUWTo.js} | 2 +- .../static/assets/fsharp-PahG7c26.js | 0 .../resources/static/assets/go-acbASCJo.js | 0 .../static/assets/graphql-BxJiqAUM.js | 0 .../static/assets/handlebars-jyf1sOa_.js} | 2 +- .../resources/static/assets/hcl-DtV1sZF8.js | 0 .../resources/static/assets/html-BPD0fe3n.js} | 2 +- .../static/assets/html.worker-CwpTb9lJ.js | 0 .../static/assets/htmlMode-Dcv1flv1.js} | 2 +- .../static/assets/index-DNgKRNTO.css | 0 .../static/assets/index-DVa6sjDi.js} | 800 ++++++------- .../resources/static/assets/ini-Kd9XrMLS.js | 0 .../resources/static/assets/java-CXBNlu9o.js | 0 .../static/assets/javascript-CPKTHyFs.js} | 2 +- .../static/assets/json.worker-BoL8UZqY.js | 0 .../static/assets/jsonMode-CtloMU4M.js} | 2 +- .../resources/static/assets/julia-cl7-CwDS.js | 0 .../static/assets/kotlin-s7OhZKlX.js | 0 .../resources/static/assets/less-9HpZscsL.js | 0 .../resources/static/assets/lexon-OrD6JF1K.js | 0 .../static/assets/liquid-BcRR0QNu.js} | 2 +- .../assets/lspLanguageFeatures-WUkMtvXB.js} | 2 +- .../resources/static/assets/lua-Cyyb5UIc.js | 0 .../resources/static/assets/m3-B8OfTtLu.js | 0 .../static/assets/markdown-BFxVWTOG.js | 0 .../resources/static/assets/mdx-CFl_thMc.js} | 2 +- .../resources/static/assets/mips-CiqrrVzr.js | 0 .../resources/static/assets/msdax-DmeGPVcC.js | 0 .../resources/static/assets/mysql-C_tMU-Nz.js | 0 .../static/assets/objective-c-BDtDVThU.js | 0 .../static/assets/pascal-vHIfCaH5.js | 0 .../static/assets/pascaligo-DtZ0uQbO.js | 0 .../resources/static/assets/perl-Ub6l9XKa.js | 0 .../resources/static/assets/pgsql-BlNEE0v7.js | 0 .../resources/static/assets/php-BBUBE1dy.js | 0 .../resources/static/assets/pla-DSh2-awV.js | 0 .../static/assets/postiats-CocnycG-.js | 0 .../static/assets/powerquery-tScXyioY.js | 0 .../static/assets/powershell-COWaemsV.js | 0 .../static/assets/protobuf-Brw8urJB.js | 0 .../resources/static/assets/pug-8SOpv6rk.js | 0 .../static/assets/python-DWshEEgQ.js} | 2 +- .../static/assets/qsharp-Bw9ernYp.js | 0 .../resources/static/assets/r-j7ic8hl3.js | 0 .../static/assets/razor-BAystUmB.js} | 2 +- .../resources/static/assets/redis-Bu5POkcn.js | 0 .../static/assets/redshift-Bs9aos_-.js | 0 .../assets/restructuredtext-CqXO7rUv.js | 0 .../resources/static/assets/ruby-zBfavPgS.js | 0 .../resources/static/assets/rust-BzKRNQWT.js | 0 .../resources/static/assets/sb-BBc9UKZt.js | 0 .../resources/static/assets/scala-D9hQfWCl.js | 0 .../static/assets/scheme-BPhDTwHR.js | 0 .../resources/static/assets/scss-CBJaRo0y.js | 0 .../resources/static/assets/shell-DiJ1NA_G.js | 0 .../static/assets/solidity-Db0IVjzk.js | 0 .../static/assets/sophia-CnS9iZB_.js | 0 .../static/assets/sparql-CJmd_6j2.js | 0 .../resources/static/assets/sql-ClhHkBeG.js | 0 .../resources/static/assets/st-CHwy0fLd.js | 0 .../resources/static/assets/swift-CnmFD0ga.js | 0 .../static/assets/systemverilog-Bs9z6M-B.js | 0 .../resources/static/assets/tcl-Dm6ycUr_.js | 0 .../static/assets/token-C3IvCEKP.svg | 0 .../static/assets/ts.worker-BH9nVgjN.js | 0 .../static/assets/tsMode-Du_CFU9D.js} | 2 +- .../resources/static/assets/twig-Csy3S7wG.js | 0 .../static/assets/typescript-SURKSaZv.js} | 2 +- .../static/assets/typespec-Btyra-wh.js | 0 .../static/assets/user-not-found-DLKrkbmQ.svg | 0 .../resources/static/assets/vb-Db0cS2oM.js | 0 .../resources/static/assets/wgsl-BTesnYfV.js | 0 .../resources/static/assets/xml-C7EMVKoq.js} | 2 +- .../resources/static/assets/yaml-B2n6_fZi.js} | 2 +- .../resources/static/conductorLogo-dark.svg | 0 .../main/resources/static/conductorLogo.png | Bin .../main/resources/static/conductorLogo.svg | 0 .../resources/static/conductorLogoSmall.png | Bin .../resources/static/conductorLogoSmall.svg | 0 .../src/main/resources/static/context.js | 0 .../main/resources/static/context.js.example | 0 .../main/resources/static/diagramDotBg.svg | 0 .../resources/static/docs/assets/docs.css | 0 .../resources/static/docs/assets/index.js | 0 .../src/main/resources/static/docs/index.html | 0 .../src/main/resources/static/enterIcon.svg | 0 .../enterprise-add-ons/integrations.webp | Bin .../src/main/resources/static/favicon.ico | Bin .../static/icons/apple-touch-icon.png | Bin .../resources/static/icons/favicon-16x16.png | Bin .../resources/static/icons/favicon-32x32.png | Bin .../resources/static/icons/icon-144x144.png | Bin .../resources/static/icons/icon-192x192.png | Bin .../resources/static/icons/icon-256x256.png | Bin .../resources/static/icons/icon-384x384.png | Bin .../resources/static/icons/icon-48x48.png | Bin .../resources/static/icons/icon-512x512.png | Bin .../resources/static/icons/icon-72x72.png | Bin .../resources/static/icons/icon-96x96.png | Bin .../main/resources/static/icons/info-icon.svg | 0 .../src/main/resources/static/index.html | 2 +- .../static/integrations-icons/airtable.svg | 0 .../static/integrations-icons/amazon.svg | 0 .../static/integrations-icons/amqp.svg | 0 .../static/integrations-icons/anthropic.svg | 0 .../static/integrations-icons/apachekafka.svg | 0 .../static/integrations-icons/asana.svg | 0 .../static/integrations-icons/aws-lambda.svg | 0 .../static/integrations-icons/aws-s3.svg | 0 .../static/integrations-icons/aws-ses.svg | 0 .../static/integrations-icons/aws-sns.svg | 0 .../static/integrations-icons/aws.svg | 0 .../integrations-icons/azure-devops.svg | 0 .../integrations-icons/azure-functions.svg | 0 .../integrations-icons/azure-storage.svg | 0 .../static/integrations-icons/azure.svg | 0 .../static/integrations-icons/azureOpenAI.svg | 0 .../integrations-icons/azure_openai.svg | 0 .../integrations-icons/azure_service_bus.svg | 0 .../static/integrations-icons/bedrock.svg | 0 .../static/integrations-icons/bitbucket.svg | 0 .../static/integrations-icons/circleci.svg | 0 .../static/integrations-icons/cloudflare.svg | 0 .../static/integrations-icons/cohere.svg | 0 .../static/integrations-icons/commonroom.svg | 0 .../static/integrations-icons/conductor.svg | 0 .../static/integrations-icons/confluent.svg | 0 .../static/integrations-icons/datadog.svg | 0 .../static/integrations-icons/default.svg | 0 .../integrations-icons/digitalocean.svg | 0 .../static/integrations-icons/discord.svg | 0 .../static/integrations-icons/discourse.svg | 0 .../static/integrations-icons/docker.svg | 0 .../static/integrations-icons/freshdesk.svg | 0 .../static/integrations-icons/gcp_pubsub.svg | 0 .../static/integrations-icons/gemini.svg | 0 .../static/integrations-icons/git.svg | 0 .../static/integrations-icons/github.svg | 0 .../static/integrations-icons/gitlab.svg | 0 .../static/integrations-icons/gmail.svg | 0 .../google-cloud-functions.svg | 0 .../google-cloud-storage.svg | 0 .../static/integrations-icons/google-docs.svg | 0 .../integrations-icons/google-drive.svg | 0 .../integrations-icons/google-sheets.svg | 0 .../integrations-icons/google-slides.svg | 0 .../integrations-icons/google_sheets.svg | 0 .../static/integrations-icons/googleads.svg | 0 .../integrations-icons/googleanalytics.svg | 0 .../integrations-icons/googlecalendar.svg | 0 .../static/integrations-icons/googledrive.svg | 0 .../integrations-icons/googlegemini.svg | 0 .../static/integrations-icons/grok.svg | 0 .../static/integrations-icons/hubspot.svg | 0 .../static/integrations-icons/huggingFace.svg | 0 .../static/integrations-icons/ibm_mq.svg | 0 .../static/integrations-icons/instaclustr.svg | 0 .../static/integrations-icons/intercom.svg | 0 .../static/integrations-icons/jira.svg | 0 .../static/integrations-icons/kafka.svg | 0 .../integrations-icons/kafka_confluent.svg | 0 .../static/integrations-icons/kafka_msk.svg | 0 .../static/integrations-icons/kubernetes.svg | 0 .../static/integrations-icons/linear.svg | 0 .../static/integrations-icons/mailgun.svg | 0 .../static/integrations-icons/menuBook.svg | 0 .../static/integrations-icons/mistral.svg | 0 .../static/integrations-icons/mistralai.svg | 0 .../static/integrations-icons/mixpanel.svg | 0 .../static/integrations-icons/mongo.svg | 0 .../static/integrations-icons/mongodb.svg | 0 .../static/integrations-icons/mongovector.svg | 0 .../static/integrations-icons/mysql.svg | 0 .../static/integrations-icons/nats.svg | 0 .../static/integrations-icons/notion.svg | 0 .../static/integrations-icons/okta.svg | 0 .../static/integrations-icons/ollama.svg | 0 .../static/integrations-icons/openAI.svg | 0 .../static/integrations-icons/pagerduty.svg | 0 .../static/integrations-icons/perplexity.svg | Bin .../static/integrations-icons/pgvector.svg | 0 .../static/integrations-icons/pinecone.svg | 0 .../static/integrations-icons/pipedrive.svg | 0 .../static/integrations-icons/postgres.svg | 0 .../static/integrations-icons/private-ai.svg | 0 .../static/integrations-icons/rabbitmq.svg | 0 .../static/integrations-icons/redis.svg | 0 .../integrations-icons/relational_db.svg | 0 .../static/integrations-icons/sendgrid.svg | 0 .../static/integrations-icons/sentry.svg | 0 .../static/integrations-icons/slack.svg | 0 .../static/integrations-icons/stripe.svg | 0 .../static/integrations-icons/symphone.svg | 0 .../static/integrations-icons/teams.svg | 0 .../static/integrations-icons/telegram.svg | 0 .../static/integrations-icons/terraform.svg | 0 .../static/integrations-icons/trello.svg | 0 .../static/integrations-icons/twilio.svg | 0 .../static/integrations-icons/vertexAI.svg | 0 .../static/integrations-icons/weaviate.svg | 0 .../static/integrations-icons/youtube.svg | 0 .../static/integrations-icons/zendesk.svg | 0 .../src/main/resources/static/logo.png | Bin .../resources/static/orkes-logo-purple-2x.png | Bin .../static/orkes-logo-purple-inverted-2x.png | Bin .../programming-language-icons/python.svg | 0 .../src/main/resources/static/robots.txt | 0 .../main/resources/static/searchIconBg.svg | 0 .../resources/static/wh-icons/github-icon.svg | 0 .../static/wh-icons/microsoft-teams-icon.svg | 0 .../static/wh-icons/send-grid-icon.svg | 0 .../resources/static/wh-icons/slack-icon.svg | 0 .../resources/static/wh-icons/stripe-icon.svg | 0 .../agentspan/runtime/ServerSmokeTest.java | 37 + .../ai/AgentChatCompleteTaskMapperTest.java | 0 .../AgentspanAIModelProviderPerUserTest.java | 46 +- .../ai/AgentspanAIModelProviderTest.java | 0 .../runtime/compiler/AgentCompilerTest.java | 0 .../compiler/GuardrailCompilerTest.java | 0 .../compiler/MultiAgentCompilerTest.java | 0 .../compiler/SynthOutputScriptTest.java | 0 .../compiler/TerminationCompilerTest.java | 0 .../runtime/compiler/ToolCompilerTest.java | 0 .../context}/RequestContextHolderTest.java | 27 +- .../controller/AgentCompileE2ETest.java | 0 .../AgentControllerSSEIntegrationTest.java | 0 .../controller/AgentDagEndpointTest.java | 0 .../controller/CredentialControllerTest.java | 0 .../CredentialMaskingIntegrationTest.java | 4 +- .../controller/EventPushEndpointTest.java | 0 .../controller/SkillControllerTest.java | 0 .../WorkerCredentialsIntegrationTest.java | 34 +- .../controller/WorkerCredentialsTest.java | 5 +- .../credentials/ConcurrentPutRaceTest.java | 1 + .../CredentialAwareHttpTaskTest.java | 1 + .../CredentialAwareMcpServiceTest.java | 1 + .../CredentialDataSourceConfigTest.java | 18 +- .../CredentialEnvSeederIntegrationTest.java | 1 + .../credentials/CredentialEnvSeederTest.java | 1 + .../CredentialResolutionServiceTest.java | 1 + .../CredentialSchemaMigratorTest.java | 0 ...ncryptedDbCredentialStoreProviderTest.java | 1 + .../ExecutionTokenServiceTest.java | 0 .../credentials/MasterKeyConfigTest.java | 0 .../SchemaMigratorUpgradePathTest.java | 1 + .../runtime/model/AgentConfigTest.java | 0 .../runtime/model/AgentSSEEventTest.java | 0 .../ClaudeAgentSdkNormalizerTest.java | 0 .../normalizer/LangChainNormalizerTest.java | 0 .../normalizer/LangGraphNormalizerTest.java | 0 .../normalizer/OpenAINormalizerTest.java | 0 .../normalizer/SkillNormalizerTest.java | 0 .../normalizer/VercelAINormalizerTest.java | 0 .../runtime/service/AgentDagServiceTest.java | 0 .../service/AgentEventListenerTest.java | 0 ...AgentEventListenerTokenRevocationTest.java | 0 .../runtime/service/AgentHumanTaskTest.java | 0 .../service/AgentServiceTokenTest.java | 4 +- .../service/AgentStreamRegistryTest.java | 0 .../service/PlanAndCompileTaskTest.java | 0 .../service/PlannerContextFetchTaskTest.java | 0 .../service/SkillRegistryServiceTest.java | 54 +- .../runtime/util/EnrichToolsScriptTest.java | 0 .../runtime/util/ModelContextWindowsTest.java | 0 .../runtime/util/ProviderValidatorTest.java | 0 .../util/SafeConditionInterpreterTest.java | 0 .../util/SchemaSubsetValidatorTest.java | 0 .../resources/application-test.properties | 3 - .../resources/skills/conductor-skill.json | 0 .../test/resources/skills/crossref-skill.json | 0 .../src/test/resources/skills/dg-skill.json | 0 .../test/resources/skills/large-skill.json | 0 .../test/resources/skills/simple-skill.json | 0 server/conductor-agentspan/build.gradle | 89 ++ .../ai/AgentChatCompleteTaskMapper.java | 0 .../runtime/ai/AgentspanAIModelProvider.java | 12 +- .../runtime/compiler/AgentCompiler.java | 0 .../runtime/compiler/GateCompiler.java | 0 .../runtime/compiler/GuardrailCompiler.java | 0 .../runtime/compiler/HumanTaskBuilder.java | 0 .../runtime/compiler/MultiAgentCompiler.java | 4 +- .../runtime/compiler/TerminationCompiler.java | 0 .../runtime/compiler/ToolCompiler.java | 20 +- .../config/AgentSpanAutoConfiguration.java | 73 ++ .../runtime/context/RequestContext.java | 31 + .../runtime/context/RequestContextHolder.java | 43 + .../runtime/controller/AgentController.java | 0 .../controller/AgentExceptionHandler.java | 0 .../CredentialMaskingResponseAdvice.java | 17 +- .../runtime/controller/SecretController.java | 10 +- .../runtime/controller/SkillController.java | 0 .../runtime/controller/WorkerController.java | 0 .../credentials/CredentialAwareHttpTask.java | 0 .../CredentialAwareHttpTaskConfig.java | 9 + .../CredentialAwareMcpService.java | 0 .../CredentialResolutionService.java | 2 + .../credentials/ExecutionTokenService.java | 0 .../credentials/KnownProviderEnvVars.java | 78 ++ .../agentspan/runtime/model/AgentConfig.java | 0 .../runtime/model/AgentExecutionDetail.java | 0 .../runtime/model/AgentExecutionSummary.java | 0 .../dev/agentspan/runtime/model/AgentRun.java | 0 .../runtime/model/AgentSSEEvent.java | 0 .../agentspan/runtime/model/AgentSummary.java | 0 .../runtime/model/CallbackConfig.java | 0 .../agentspan/runtime/model/CliConfig.java | 0 .../runtime/model/CodeExecutionConfig.java | 0 .../runtime/model/CompileResponse.java | 0 .../model/CreateTrackingWorkflowRequest.java | 0 .../model/CreateTrackingWorkflowResponse.java | 0 .../runtime/model/GuardrailConfig.java | 0 .../runtime/model/HandoffConfig.java | 0 .../runtime/model/InjectTaskRequest.java | 0 .../runtime/model/InjectTaskResponse.java | 0 .../runtime/model/InspectPlanRequest.java | 0 .../agentspan/runtime/model/MemoryConfig.java | 0 .../runtime/model/OutputTypeConfig.java | 0 .../runtime/model/PrefillToolCallConfig.java | 0 .../runtime/model/PromptTemplateRef.java | 0 .../agentspan/runtime/model/StartRequest.java | 0 .../runtime/model/StartResponse.java | 0 .../runtime/model/TaskListResponse.java | 0 .../runtime/model/TerminationConfig.java | 0 .../runtime/model/ThinkingConfig.java | 0 .../agentspan/runtime/model/ToolConfig.java | 0 .../agentspan/runtime/model/WorkerRef.java | 0 .../model/credentials/CredentialMeta.java | 0 .../model/credentials/ResolveRequest.java | 0 .../model/credentials/ResolveResponse.java | 0 .../model/skill/SkillDeployRequest.java | 0 .../runtime/model/skill/SkillDetail.java | 0 .../runtime/model/skill/SkillFileContent.java | 0 .../runtime/model/skill/SkillFileEntry.java | 0 .../runtime/model/skill/SkillSummary.java | 0 .../normalizer/AgentConfigNormalizer.java | 0 .../normalizer/ClaudeAgentSdkNormalizer.java | 0 .../normalizer/GoogleADKNormalizer.java | 0 .../normalizer/LangChainNormalizer.java | 0 .../normalizer/LangGraphNormalizer.java | 0 .../normalizer/NormalizerRegistry.java | 0 .../runtime/normalizer/OpenAINormalizer.java | 0 .../runtime/normalizer/SkillNormalizer.java | 0 .../normalizer/VercelAINormalizer.java | 0 .../runtime/service/AgentDagService.java | 0 .../runtime/service/AgentEventListener.java | 0 .../runtime/service/AgentHumanTask.java | 0 .../runtime/service/AgentHumanTaskConfig.java | 10 + .../runtime/service/AgentService.java | 73 +- .../runtime/service/AgentStreamRegistry.java | 0 .../runtime/service/ListApiToolsTask.java | 0 .../service/ListApiToolsTaskConfig.java | 0 .../runtime/service/PlanAndCompileTask.java | 0 .../service/PlanAndCompileTaskConfig.java | 0 .../service/PlannerContextFetchTask.java | 0 .../PlannerContextFetchTaskConfig.java | 0 .../runtime/service/SkillRegistryService.java | 272 +---- .../runtime/spi}/CredentialStoreProvider.java | 8 +- .../runtime/spi/SecretOutputMasker.java | 25 + .../runtime/spi/SkillMetadataDAO.java | 51 + .../runtime/spi}/SkillPackageStore.java | 2 +- .../runtime/spi}/StoredSkillPackage.java | 2 +- .../dev/agentspan/runtime/tasks/Join.java | 2 + .../agentspan/runtime/util/EmbeddedMode.java | 31 + .../runtime/util/JavaScriptBuilder.java | 0 .../runtime/util/ModelContextWindows.java | 0 .../agentspan/runtime/util/ModelParser.java | 0 .../runtime/util/ProviderValidator.java | 15 +- .../util/SafeConditionInterpreter.java | 0 .../util/SafeConditionParseException.java | 0 .../runtime/util/SchemaSubsetValidator.java | 0 .../runtime/util/WorkflowTaskUtils.java | 0 ...ot.autoconfigure.AutoConfiguration.imports | 1 + server/docs/agentspan-as-a-library.md | 722 +++++++++++ server/gradle.properties | 5 +- server/settings.gradle | 4 +- .../runtime/auth/ApiKeyRepository.java | 101 -- .../agentspan/runtime/auth/AuthFilter.java | 168 --- .../runtime/auth/AuthProperties.java | 37 - .../runtime/auth/AuthUserSeeder.java | 44 - .../runtime/auth/RequestContext.java | 28 - .../runtime/auth/RequestContextHolder.java | 43 - .../java/dev/agentspan/runtime/auth/User.java | 25 - .../runtime/auth/UserRepository.java | 127 -- .../runtime/controller/AuthController.java | 71 -- .../credentials/CredentialOutputMasker.java | 32 - .../runtime/auth/ApiKeyRepositoryTest.java | 68 -- .../runtime/auth/AuthFilterTest.java | 146 --- .../runtime/auth/AuthUserSeederTest.java | 48 - .../runtime/auth/UserRepositoryTest.java | 113 -- .../controller/AuthControllerTest.java | 67 -- .../components/Sidebar/sidebarCoreItems.tsx | 8 +- ui/src/pages/secrets/SecretsPage.tsx | 39 +- .../secrets/__tests__/LoginDialog.test.tsx | 50 - .../secrets/__tests__/SecretsPage.test.tsx | 8 - .../secrets/__tests__/useSecretAuth.test.ts | 40 - .../pages/secrets/components/LoginDialog.tsx | 84 -- ui/src/pages/secrets/hooks/useSecretAuth.ts | 33 - ui/src/pages/secrets/hooks/useSecretsApi.ts | 13 +- ui/src/pages/secrets/types.ts | 10 - 891 files changed, 23131 insertions(+), 12824 deletions(-) create mode 100644 .github/workflows/release-server-maven.yml create mode 100644 cli/auth/auth0.go create mode 100644 cli/auth/auth0_test.go create mode 100644 cli/auth/orkes.go create mode 100644 cli/auth/orkes_test.go create mode 100644 cli/auth/pkce.go create mode 100644 cli/auth/pkce_test.go create mode 100644 cli/config/token.go create mode 120000 docs/java-sdk delete mode 100644 sdk/csharp/examples/27_UserProxyAgent/Example27UserProxyAgent.csproj delete mode 100644 sdk/csharp/examples/27_UserProxyAgent/Program.cs delete mode 100644 sdk/csharp/src/Agentspan/UserProxyAgent.cs create mode 100644 sdk/csharp/tests/Agentspan.OpenAI.Tests/CliToolTests.cs create mode 100644 sdk/java/docs/agent-client-api.md create mode 100644 sdk/java/docs/agent-runtime-api.md create mode 100644 sdk/java/docs/agent-schema.json create mode 100644 sdk/java/docs/agent-schema.md create mode 100644 sdk/java/docs/agent-structure.md create mode 100644 sdk/java/docs/api-reference.md create mode 100644 sdk/java/docs/concepts/agents.md create mode 100644 sdk/java/docs/concepts/guardrails.md create mode 100644 sdk/java/docs/concepts/multi-agent.md create mode 100644 sdk/java/docs/concepts/scheduling.md create mode 100644 sdk/java/docs/concepts/skills.md create mode 100644 sdk/java/docs/concepts/termination.md create mode 100644 sdk/java/docs/concepts/tools.md create mode 100644 sdk/java/docs/frameworks/google-adk.md create mode 100644 sdk/java/docs/frameworks/langchain4j.md create mode 100644 sdk/java/docs/frameworks/openai.md create mode 100644 sdk/java/docs/generated/AgentConfigModel.java create mode 100644 sdk/java/docs/generated/agent_config.py create mode 100644 sdk/java/docs/generated/generate.py create mode 100644 sdk/java/docs/getting-started.md create mode 100644 sdk/java/docs/index.md create mode 100644 sdk/java/docs/mkdocs.yml create mode 100644 sdk/java/docs/spring-boot.md create mode 100644 sdk/java/e2e/Suite11bOpenAIAgent.java create mode 100644 sdk/java/e2e/Suite17ConfigSerialization.java delete mode 100644 sdk/java/e2e/Suite17NewParity.java create mode 100644 sdk/java/e2e/Suite19ManualStrategy.java rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example01BasicAgent.java (65%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example02Tools.java (71%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example02aSimpleTools.java (73%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example02bMultiStepTools.java (88%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example02cTypedToolArgs.java (82%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example03StructuredOutput.java (80%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example04HttpAndMcpTools.java (85%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example05Handoffs.java (82%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example06SequentialPipeline.java (85%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example07ParallelAgents.java (84%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example08RouterAgent.java (85%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example09HumanInTheLoop.java (87%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example09bHandoffHumanInTheLoop.java (88%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example108PlanExecuteRefs.java (91%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example10Guardrails.java (79%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example115PlannerContext.java (96%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example11Streaming.java (90%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example12LongRunning.java (86%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example13HierarchicalAgents.java (90%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example14ExistingWorkers.java (87%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example15AgentDiscussion.java (89%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example16CredentialsTool.java (86%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example16RandomStrategy.java (83%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example17SwarmOrchestration.java (85%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example18ManualSelection.java (88%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example19ComposableTermination.java (80%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example20ConstrainedTransitions.java (86%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example21RegexGuardrails.java (80%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example22LlmGuardrails.java (83%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example23TokenTracking.java (83%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example29AgentIntroductions.java (89%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example31ToolInputGuardrail.java (78%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example32HumanGuardrail.java (83%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example33ExternalWorkers.java (87%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example33SingleTurnTool.java (72%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example34PromptTemplates.java (77%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example35StandaloneGuardrails.java (97%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example36SimpleAgentGuardrails.java (83%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example37FixGuardrail.java (80%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example38TechTrends.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example41SequentialPipelineTools.java (92%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example42SecurityTesting.java (89%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example43DataSecurityPipeline.java (90%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example44SafetyGuardrails.java (90%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example45AgentTool.java (91%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example46TransferControl.java (86%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example47Callbacks.java (84%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example48Planner.java (83%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example49IncludeContents.java (84%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example50ThinkingConfig.java (80%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example51SharedState.java (86%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example52NestedStrategies.java (88%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example53AgentLifecycleCallbacks.java (86%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example54SoftwareBugAssistant.java (94%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example55MlEngineeringPipeline.java (93%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example56RagAgent.java (90%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example57PlanDryRun.java (87%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example58ScatterGather.java (88%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example59CodingAgent.java (89%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example64SwarmWithTools.java (84%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example65ParallelWithTools.java (83%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example66HandoffToParallel.java (88%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example67RouterToSequential.java (89%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example68ContextCondensation.java (94%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example69Skills.java (76%) create mode 100644 sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example70AnnotatedAgent.java rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Example99ScheduledAgent.java (92%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/Settings.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/VerifyHandoffs.java (88%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/VerifyRouting.java (84%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example00HelloWorld.java (69%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example01BasicAgent.java (70%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example02FunctionTools.java (89%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example03StructuredOutput.java (92%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example04SubAgents.java (94%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example05GenerationConfig.java (84%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example06Streaming.java (84%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example07OutputKeyState.java (91%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example08InstructionTemplating.java (90%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example09MultiToolAgent.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example10HierarchicalAgents.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example11SequentialAgent.java (88%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example12ParallelAgent.java (86%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example13LoopAgent.java (85%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example14Callbacks.java (92%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example15GlobalInstruction.java (90%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example16CustomerService.java (94%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example17FinancialAdvisor.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example18OrderProcessing.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example19SupplyChain.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example20BlogWriter.java (93%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example21AgentTool.java (94%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example22TransferControl.java (90%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example23Callbacks.java (91%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example24Planner.java (90%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example25CamelSecurity.java (92%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example26SafetyGuardrails.java (93%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example27SecurityAgent.java (94%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example28MoviePipeline.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example29IncludeContents.java (86%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example30ThinkingConfig.java (91%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example31SharedState.java (87%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example32NestedStrategies.java (91%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example33SoftwareBugAssistant.java (96%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example34MlEngineering.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example35RagAgent.java (96%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example36BuiltInTools.java (82%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example37DeployAndServe.java (85%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/adk/Example38AgentspanGuardrails.java (87%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example01HelloWorld.java (82%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example02ReactWithTools.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example03CustomTools.java (93%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example04StructuredOutput.java (96%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example05PromptTemplates.java (94%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example06ChatHistory.java (91%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example07MemoryAgent.java (90%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example08MultiToolAgent.java (94%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example09MathCalculator.java (97%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example10WebSearchAgent.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example11CodeReviewAgent.java (96%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example12DocumentSummarizer.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example13CustomerServiceAgent.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example14ResearchAssistant.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example15DataAnalyst.java (97%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example16ContentWriter.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example17SqlAgent.java (98%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example18EmailDrafter.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example19FactChecker.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example20TranslationAgent.java (96%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example21SentimentAnalysis.java (96%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example22ClassificationAgent.java (96%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example23RecommendationAgent.java (96%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example24OutputParsers.java (96%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example25AdvancedOrchestration.java (97%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/Example26AgentspanGuardrails.java (83%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/ExampleCredentials.java (90%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langchain/ExamplePipeline.java (92%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langgraph/Example01HelloWorld.java (82%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langgraph/Example02ReactWithTools.java (90%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langgraph/Example03Memory.java (87%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langgraph/Example04SimpleStateGraph.java (92%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langgraph/Example05ToolNode.java (92%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langgraph/Example06ConditionalRouting.java (93%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langgraph/Example07SystemPrompt.java (89%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langgraph/Example08StructuredOutput.java (92%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langgraph/Example09MathAgent.java (92%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langgraph/Example10ResearchAgent.java (95%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/langgraph/Example11CustomerSupport.java (94%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/openai/Example01BasicAgent.java (71%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/openai/Example02FunctionTools.java (89%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/openai/Example03StructuredOutput.java (84%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/openai/Example04Handoffs.java (91%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/openai/Example05Guardrails.java (83%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/openai/Example06ModelSettings.java (83%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/openai/Example07Streaming.java (81%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/openai/Example08AgentAsTool.java (91%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/openai/Example09DynamicInstructions.java (87%) rename sdk/java/examples/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/examples/openai/Example10MultiModel.java (91%) delete mode 100644 sdk/java/spring/src/main/java/ai/agentspan/spring/AgentspanAutoConfiguration.java delete mode 100644 sdk/java/spring/src/main/java/ai/agentspan/spring/AgentspanProperties.java create mode 100644 sdk/java/spring/src/main/java/org/conductoross/conductor/ai/spring/AgentAutoConfiguration.java create mode 100644 sdk/java/spring/src/main/java/org/conductoross/conductor/ai/spring/AgentCatalog.java create mode 100644 sdk/java/spring/src/main/java/org/conductoross/conductor/ai/spring/AgentProperties.java delete mode 100644 sdk/java/spring/src/test/java/ai/agentspan/spring/AgentspanAutoConfigurationTest.java create mode 100644 sdk/java/spring/src/test/java/org/conductoross/conductor/ai/spring/AgentAutoConfigurationTest.java create mode 100644 sdk/java/spring/src/test/java/org/conductoross/conductor/ai/spring/AgentCatalogTest.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/AgentConfig.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/Agentspan.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/ClaudeCode.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/Credentials.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/UserProxyAgent.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/execution/CliConfig.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/internal/HttpApi.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/internal/SseClient.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/internal/WorkerCredentialFetcher.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/internal/WorkerHttp.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/internal/WorkerManager.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/model/ToolContext.java delete mode 100644 sdk/java/src/main/java/ai/agentspan/schedule/ScheduleInfo.java rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/Agent.java (66%) create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/AgentConfig.java rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/AgentRuntime.java (52%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/CallbackHandler.java (90%) create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/AgentDef.java rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/annotations/GuardrailDef.java (84%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/annotations/Tool.java (92%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/enums/AgentStatus.java (85%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/enums/EventType.java (81%) create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/enums/Framework.java rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/enums/OnFail.java (73%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/enums/Position.java (70%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/enums/Strategy.java (80%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/exceptions/AgentAPIException.java (93%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/exceptions/AgentNotFoundException.java (92%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/exceptions/AgentspanException.java (89%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/exceptions/CredentialAuthException.java (91%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/exceptions/CredentialNotFoundException.java (95%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/exceptions/CredentialRateLimitException.java (92%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/exceptions/CredentialServiceException.java (93%) create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/execution/CliCommandExecutor.java create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/execution/CliConfig.java rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/execution/CodeExecutor.java (70%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/execution/DockerCodeExecutor.java (81%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/execution/ExecutionResult.java (69%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/frameworks/AdkBridge.java (86%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/frameworks/LangChain4jAgent.java (86%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/frameworks/LangChainBridge.java (84%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/frameworks/OpenAIAgent.java (80%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/gate/TextGate.java (96%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/guardrail/Guardrail.java (81%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/guardrail/LLMGuardrail.java (68%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/guardrail/RegexGuardrail.java (71%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/handoff/Handoff.java (92%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/handoff/OnCondition.java (95%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/handoff/OnTextMention.java (93%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/handoff/OnToolResult.java (84%) create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentClient.java rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/internal/AgentConfigSerializer.java (73%) create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentRegistry.java create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentRequest.java create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentStatusResponse.java create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/internal/CredentialContext.java rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/internal/JsonMapper.java (97%) create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/internal/PendingTool.java create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/internal/RespondBody.java create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/internal/SseClient.java create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/internal/StartResponse.java rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/internal/ToolRegistry.java (82%) create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/internal/WorkerCredentialFetcher.java create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/internal/WorkerManager.java rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/model/AgentEvent.java (69%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/model/AgentHandle.java (51%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/model/AgentResult.java (88%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/model/AgentStream.java (79%) create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/model/CompileResponse.java create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/model/ConversationMemory.java rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/model/CredentialFile.java (84%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/model/DeploymentInfo.java (83%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/model/GuardrailDef.java (51%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/model/GuardrailResult.java (96%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/model/PrefillToolCall.java (83%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/model/PromptTemplate.java (85%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/model/TokenUsage.java (88%) create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/model/ToolContext.java rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/model/ToolDef.java (55%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/openai/GPTAssistantAgent.java (70%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/plans/Action.java (96%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/plans/Context.java (99%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/plans/Generate.java (98%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/plans/Op.java (91%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/plans/Plan.java (80%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/plans/PlanValues.java (97%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/plans/Ref.java (97%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/plans/Step.java (98%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/plans/Validation.java (97%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/schedule/Schedule.java (55%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/schedule/ScheduleException.java (72%) create mode 100644 sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/ScheduleInfo.java rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/schedule/Schedules.java (52%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/skill/Skill.java (85%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/skill/SkillLoadError.java (90%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/termination/AndTermination.java (80%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/termination/MaxMessageTermination.java (94%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/termination/OrTermination.java (80%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/termination/StopMessageTermination.java (95%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/termination/TerminationCondition.java (95%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/termination/TerminationResult.java (84%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/termination/TextMentionTermination.java (96%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/termination/TokenUsageTermination.java (85%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai/tools}/AgentTool.java (64%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/tools/HttpTool.java (80%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/tools/HumanTool.java (95%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/tools/McpTool.java (77%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/tools/MediaTools.java (89%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/tools/PdfTool.java (94%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai/tools}/RagTools.java (77%) rename sdk/java/src/main/java/{ai/agentspan => org/conductoross/conductor/ai}/tools/WaitForMessageTool.java (95%) delete mode 100644 sdk/java/src/test/java/ai/agentspan/CredentialsTest.java create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/AgentAnnotationTest.java rename sdk/java/src/test/java/{ai/agentspan => org/conductoross/conductor/ai}/AgentBuilderTest.java (71%) rename sdk/java/src/test/java/{ai/agentspan => org/conductoross/conductor/ai}/ModelExecutionIdTest.java (77%) rename sdk/java/src/test/java/{ai/agentspan => org/conductoross/conductor/ai}/SerializerTest.java (57%) create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/ToolContextCredentialsTest.java create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/exceptions/ExceptionsTest.java create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/execution/CliCommandExecutorTest.java create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/execution/CodeExecutorAsToolTest.java create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/frameworks/AdkBridgeTest.java create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/handoff/HandoffTest.java rename sdk/java/src/test/java/{ai/agentspan => org/conductoross/conductor/ai}/internal/ToolRegistryTest.java (56%) create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/internal/WorkerManagerDomainTest.java create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/internal/WorkerManagerThreadCountTest.java create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/internal/WorkerManagerTimeoutTest.java create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/model/AgentHandleErrorTest.java create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/model/ModelTest.java rename sdk/java/src/test/java/{ai/agentspan => org/conductoross/conductor/ai}/plans/ContextTest.java (89%) rename sdk/java/src/test/java/{ai/agentspan => org/conductoross/conductor/ai}/plans/OpTest.java (56%) create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/plans/PlansTest.java rename sdk/java/src/test/java/{ai/agentspan => org/conductoross/conductor/ai}/schedule/ScheduleIntegrationTest.java (53%) rename sdk/java/src/test/java/{ai/agentspan => org/conductoross/conductor/ai}/schedule/ScheduleTest.java (79%) create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/termination/TerminationConditionsTest.java create mode 100644 sdk/java/src/test/java/org/conductoross/conductor/ai/tools/ToolsTest.java delete mode 100644 sdk/python/examples/27_user_proxy_agent.py create mode 100644 sdk/python/src/agentspan/agents/_internal/token_utils.py create mode 100644 sdk/python/tests/unit/test_token_utils.py delete mode 100644 sdk/typescript/examples/27-user-proxy-agent.ts create mode 100644 server/conductor-agentspan-server/build.gradle rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/AgentRuntime.java (95%) create mode 100644 server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/auth/AuthFilter.java rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/config/CorsConfig.java (100%) rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/config/ShutdownConfig.java (100%) rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/config/StaticDocsConfig.java (100%) rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/config/UiRoutingConfig.java (100%) rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/credentials/CredentialDataSourceConfig.java (100%) rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java (70%) rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/credentials/CredentialSchemaMigrator.java (100%) rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProvider.java (99%) rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/credentials/MasterKeyConfig.java (100%) create mode 100644 server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/NoOpSecretOutputMasker.java rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/metrics/MetricsFilterConfig.java (100%) rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/service/skill/ConductorPayloadSkillPackageStore.java (97%) create mode 100644 server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java rename server/{ => conductor-agentspan-server}/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillPackageStore.java (97%) rename server/{ => conductor-agentspan-server}/src/main/resources/application-postgres.properties (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/application-rag.properties (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/application.properties (95%) rename server/{ => conductor-agentspan-server}/src/main/resources/banner.txt (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/log4j2.xml (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/schema-credentials-postgres.sql (70%) rename server/{ => conductor-agentspan-server}/src/main/resources/schema-credentials.sql (62%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/agentspan-icon.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/agentspan-logo-dark.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/agentspan-logo-light.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/agentspan-logo-small.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/agentspan-logo.svg (100%) rename server/{src/main/resources/static/assets/DailyMotion-Bik8u2gQ.js => conductor-agentspan-server/src/main/resources/static/assets/DailyMotion-DuBKwtXw.js} (97%) rename server/{src/main/resources/static/assets/Facebook-DL8ElG-z.js => conductor-agentspan-server/src/main/resources/static/assets/Facebook-D-v5cNby.js} (98%) rename server/{src/main/resources/static/assets/FilePlayer-C3WXHWsE.js => conductor-agentspan-server/src/main/resources/static/assets/FilePlayer-BGKWD3yK.js} (99%) rename server/{src/main/resources/static/assets/Kaltura-CbgdFPgd.js => conductor-agentspan-server/src/main/resources/static/assets/Kaltura-CeCCDjbU.js} (97%) rename server/{src/main/resources/static/assets/Mixcloud-CnLXhrD1.js => conductor-agentspan-server/src/main/resources/static/assets/Mixcloud-adpnMd5P.js} (97%) rename server/{src/main/resources/static/assets/Mux-Bx64MX_j.js => conductor-agentspan-server/src/main/resources/static/assets/Mux-8MezvdiC.js} (98%) rename server/{src/main/resources/static/assets/Preview-CaZuZa6m.js => conductor-agentspan-server/src/main/resources/static/assets/Preview-BMOGze4w.js} (97%) rename server/{src/main/resources/static/assets/SoundCloud-C1lS-_i6.js => conductor-agentspan-server/src/main/resources/static/assets/SoundCloud-_btX2qkW.js} (97%) rename server/{src/main/resources/static/assets/Streamable-Ctz4Qtsg.js => conductor-agentspan-server/src/main/resources/static/assets/Streamable-B4sBlscb.js} (97%) rename server/{src/main/resources/static/assets/Twitch-DRtDJx-g.js => conductor-agentspan-server/src/main/resources/static/assets/Twitch-qhW1EEt2.js} (97%) rename server/{src/main/resources/static/assets/Vidyard-L9nAMazd.js => conductor-agentspan-server/src/main/resources/static/assets/Vidyard-BJO2x236.js} (97%) rename server/{src/main/resources/static/assets/Vimeo-BKwdn9Eh.js => conductor-agentspan-server/src/main/resources/static/assets/Vimeo-C4XIslfc.js} (98%) rename server/{src/main/resources/static/assets/Wistia-DaMqyZOi.js => conductor-agentspan-server/src/main/resources/static/assets/Wistia-DSoqhXcd.js} (98%) rename server/{src/main/resources/static/assets/YouTube-BR2c3EbR.js => conductor-agentspan-server/src/main/resources/static/assets/YouTube-CLOSIIy_.js} (98%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/abap-DLDM7-KI.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/apex-DNDY2TF8.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/azcli-Y6nb8tq_.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/bat-BwHxbl9M.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/bicep-CFznDFnq.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/cameligo-Bf6VGUru.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/clojure-Dnu-v4kV.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/codicon-ngg6Pgfi.ttf (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/coffee-Bd8akH9Z.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/cpp-BbWJElDN.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/csharp-Co3qMtFm.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/csp-D-4FJmMZ.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/css-DdJfP1eB.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/css.worker-DBVD8oXr.js (100%) rename server/{src/main/resources/static/assets/cssMode-DczLDKZ6.js => conductor-agentspan-server/src/main/resources/static/assets/cssMode-Bg4Vg_j9.js} (91%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/cypher-cTPe9QuQ.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/dart-BOtBlQCF.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/dockerfile-BG73LgW2.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/ecl-BEgZUVRK.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/elixir-BkW5O-1t.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/email-not-verified-C6p1YrlM.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/flow9-BeJ5waoc.js (100%) rename server/{src/main/resources/static/assets/freemarker2-DByYdBA1.js => conductor-agentspan-server/src/main/resources/static/assets/freemarker2-DiIhUWTo.js} (99%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/fsharp-PahG7c26.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/go-acbASCJo.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/graphql-BxJiqAUM.js (100%) rename server/{src/main/resources/static/assets/handlebars-BkkyRqxt.js => conductor-agentspan-server/src/main/resources/static/assets/handlebars-jyf1sOa_.js} (98%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/hcl-DtV1sZF8.js (100%) rename server/{src/main/resources/static/assets/html-DC5114b3.js => conductor-agentspan-server/src/main/resources/static/assets/html-BPD0fe3n.js} (98%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/html.worker-CwpTb9lJ.js (100%) rename server/{src/main/resources/static/assets/htmlMode-B4KdkHsA.js => conductor-agentspan-server/src/main/resources/static/assets/htmlMode-Dcv1flv1.js} (92%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/index-DNgKRNTO.css (100%) rename server/{src/main/resources/static/assets/index-DiybVIaQ.js => conductor-agentspan-server/src/main/resources/static/assets/index-DVa6sjDi.js} (70%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/ini-Kd9XrMLS.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/java-CXBNlu9o.js (100%) rename server/{src/main/resources/static/assets/javascript-PBX7W67C.js => conductor-agentspan-server/src/main/resources/static/assets/javascript-CPKTHyFs.js} (84%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/json.worker-BoL8UZqY.js (100%) rename server/{src/main/resources/static/assets/jsonMode-BbtaPfU1.js => conductor-agentspan-server/src/main/resources/static/assets/jsonMode-CtloMU4M.js} (98%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/julia-cl7-CwDS.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/kotlin-s7OhZKlX.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/less-9HpZscsL.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/lexon-OrD6JF1K.js (100%) rename server/{src/main/resources/static/assets/liquid-DnrZBu1f.js => conductor-agentspan-server/src/main/resources/static/assets/liquid-BcRR0QNu.js} (98%) rename server/{src/main/resources/static/assets/lspLanguageFeatures-DRGgTbma.js => conductor-agentspan-server/src/main/resources/static/assets/lspLanguageFeatures-WUkMtvXB.js} (99%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/lua-Cyyb5UIc.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/m3-B8OfTtLu.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/markdown-BFxVWTOG.js (100%) rename server/{src/main/resources/static/assets/mdx-UI-3kEYu.js => conductor-agentspan-server/src/main/resources/static/assets/mdx-CFl_thMc.js} (98%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/mips-CiqrrVzr.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/msdax-DmeGPVcC.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/mysql-C_tMU-Nz.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/objective-c-BDtDVThU.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/pascal-vHIfCaH5.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/pascaligo-DtZ0uQbO.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/perl-Ub6l9XKa.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/pgsql-BlNEE0v7.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/php-BBUBE1dy.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/pla-DSh2-awV.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/postiats-CocnycG-.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/powerquery-tScXyioY.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/powershell-COWaemsV.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/protobuf-Brw8urJB.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/pug-8SOpv6rk.js (100%) rename server/{src/main/resources/static/assets/python-B1zuplsO.js => conductor-agentspan-server/src/main/resources/static/assets/python-DWshEEgQ.js} (98%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/qsharp-Bw9ernYp.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/r-j7ic8hl3.js (100%) rename server/{src/main/resources/static/assets/razor-D6US3qRg.js => conductor-agentspan-server/src/main/resources/static/assets/razor-BAystUmB.js} (99%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/redis-Bu5POkcn.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/redshift-Bs9aos_-.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/restructuredtext-CqXO7rUv.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/ruby-zBfavPgS.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/rust-BzKRNQWT.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/sb-BBc9UKZt.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/scala-D9hQfWCl.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/scheme-BPhDTwHR.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/scss-CBJaRo0y.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/shell-DiJ1NA_G.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/solidity-Db0IVjzk.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/sophia-CnS9iZB_.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/sparql-CJmd_6j2.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/sql-ClhHkBeG.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/st-CHwy0fLd.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/swift-CnmFD0ga.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/systemverilog-Bs9z6M-B.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/tcl-Dm6ycUr_.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/token-C3IvCEKP.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/ts.worker-BH9nVgjN.js (100%) rename server/{src/main/resources/static/assets/tsMode-B5_2LUKH.js => conductor-agentspan-server/src/main/resources/static/assets/tsMode-Du_CFU9D.js} (99%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/twig-Csy3S7wG.js (100%) rename server/{src/main/resources/static/assets/typescript-n8wEprT6.js => conductor-agentspan-server/src/main/resources/static/assets/typescript-SURKSaZv.js} (98%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/typespec-Btyra-wh.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/user-not-found-DLKrkbmQ.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/vb-Db0cS2oM.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/assets/wgsl-BTesnYfV.js (100%) rename server/{src/main/resources/static/assets/xml-Vks8wc7o.js => conductor-agentspan-server/src/main/resources/static/assets/xml-C7EMVKoq.js} (96%) rename server/{src/main/resources/static/assets/yaml-B7WL1ssG.js => conductor-agentspan-server/src/main/resources/static/assets/yaml-B2n6_fZi.js} (98%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/conductorLogo-dark.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/conductorLogo.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/conductorLogo.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/conductorLogoSmall.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/conductorLogoSmall.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/context.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/context.js.example (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/diagramDotBg.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/docs/assets/docs.css (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/docs/assets/index.js (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/docs/index.html (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/enterIcon.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/enterprise-add-ons/integrations.webp (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/favicon.ico (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/icons/apple-touch-icon.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/icons/favicon-16x16.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/icons/favicon-32x32.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/icons/icon-144x144.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/icons/icon-192x192.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/icons/icon-256x256.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/icons/icon-384x384.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/icons/icon-48x48.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/icons/icon-512x512.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/icons/icon-72x72.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/icons/icon-96x96.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/icons/info-icon.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/index.html (99%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/airtable.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/amazon.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/amqp.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/anthropic.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/apachekafka.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/asana.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/aws-lambda.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/aws-s3.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/aws-ses.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/aws-sns.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/aws.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/azure-devops.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/azure-functions.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/azure-storage.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/azure.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/azureOpenAI.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/azure_openai.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/azure_service_bus.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/bedrock.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/bitbucket.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/circleci.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/cloudflare.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/cohere.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/commonroom.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/conductor.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/confluent.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/datadog.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/default.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/digitalocean.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/discord.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/discourse.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/docker.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/freshdesk.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/gcp_pubsub.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/gemini.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/git.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/github.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/gitlab.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/gmail.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/google-cloud-functions.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/google-cloud-storage.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/google-docs.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/google-drive.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/google-sheets.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/google-slides.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/google_sheets.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/googleads.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/googleanalytics.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/googlecalendar.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/googledrive.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/googlegemini.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/grok.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/hubspot.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/huggingFace.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/ibm_mq.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/instaclustr.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/intercom.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/jira.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/kafka.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/kafka_confluent.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/kafka_msk.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/kubernetes.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/linear.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/mailgun.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/menuBook.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/mistral.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/mistralai.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/mixpanel.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/mongo.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/mongodb.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/mongovector.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/mysql.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/nats.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/notion.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/okta.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/ollama.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/openAI.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/pagerduty.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/perplexity.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/pgvector.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/pinecone.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/pipedrive.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/postgres.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/private-ai.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/rabbitmq.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/redis.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/relational_db.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/sendgrid.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/sentry.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/slack.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/stripe.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/symphone.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/teams.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/telegram.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/terraform.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/trello.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/twilio.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/vertexAI.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/weaviate.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/youtube.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/integrations-icons/zendesk.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/logo.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/orkes-logo-purple-2x.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/orkes-logo-purple-inverted-2x.png (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/programming-language-icons/python.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/robots.txt (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/searchIconBg.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/wh-icons/github-icon.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/wh-icons/microsoft-teams-icon.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/wh-icons/send-grid-icon.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/wh-icons/slack-icon.svg (100%) rename server/{ => conductor-agentspan-server}/src/main/resources/static/wh-icons/stripe-icon.svg (100%) create mode 100644 server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ServerSmokeTest.java rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/ai/AgentChatCompleteTaskMapperTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderPerUserTest.java (74%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/compiler/AgentCompilerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/compiler/GuardrailCompilerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/compiler/MultiAgentCompilerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/compiler/SynthOutputScriptTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/compiler/TerminationCompilerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/compiler/ToolCompilerTest.java (100%) rename server/{src/test/java/dev/agentspan/runtime/auth => conductor-agentspan-server/src/test/java/dev/agentspan/runtime/context}/RequestContextHolderTest.java (63%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/controller/AgentCompileE2ETest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/controller/AgentControllerSSEIntegrationTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/controller/AgentDagEndpointTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/controller/CredentialControllerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/controller/CredentialMaskingIntegrationTest.java (97%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/controller/EventPushEndpointTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/controller/SkillControllerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/controller/WorkerCredentialsIntegrationTest.java (88%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/controller/WorkerCredentialsTest.java (98%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/credentials/ConcurrentPutRaceTest.java (98%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskTest.java (98%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareMcpServiceTest.java (98%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/credentials/CredentialDataSourceConfigTest.java (79%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/credentials/CredentialEnvSeederIntegrationTest.java (96%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/credentials/CredentialEnvSeederTest.java (99%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/credentials/CredentialResolutionServiceTest.java (98%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/credentials/CredentialSchemaMigratorTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProviderTest.java (98%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/credentials/ExecutionTokenServiceTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/credentials/MasterKeyConfigTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/credentials/SchemaMigratorUpgradePathTest.java (98%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/model/AgentConfigTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/model/AgentSSEEventTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/normalizer/ClaudeAgentSdkNormalizerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/normalizer/LangChainNormalizerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/normalizer/LangGraphNormalizerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/normalizer/OpenAINormalizerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/normalizer/SkillNormalizerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/normalizer/VercelAINormalizerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/service/AgentDagServiceTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/service/AgentEventListenerTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/service/AgentEventListenerTokenRevocationTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/service/AgentHumanTaskTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/service/AgentServiceTokenTest.java (98%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/service/AgentStreamRegistryTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/service/PlanAndCompileTaskTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/service/PlannerContextFetchTaskTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/service/SkillRegistryServiceTest.java (81%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/util/EnrichToolsScriptTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/util/ModelContextWindowsTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/util/ProviderValidatorTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/util/SafeConditionInterpreterTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/java/dev/agentspan/runtime/util/SchemaSubsetValidatorTest.java (100%) rename server/{ => conductor-agentspan-server}/src/test/resources/application-test.properties (93%) rename server/{ => conductor-agentspan-server}/src/test/resources/skills/conductor-skill.json (100%) rename server/{ => conductor-agentspan-server}/src/test/resources/skills/crossref-skill.json (100%) rename server/{ => conductor-agentspan-server}/src/test/resources/skills/dg-skill.json (100%) rename server/{ => conductor-agentspan-server}/src/test/resources/skills/large-skill.json (100%) rename server/{ => conductor-agentspan-server}/src/test/resources/skills/simple-skill.json (100%) create mode 100644 server/conductor-agentspan/build.gradle rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ai/AgentChatCompleteTaskMapper.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/ai/AgentspanAIModelProvider.java (96%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/compiler/GateCompiler.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/compiler/GuardrailCompiler.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/compiler/HumanTaskBuilder.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java (99%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/compiler/TerminationCompiler.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/compiler/ToolCompiler.java (98%) create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/config/AgentSpanAutoConfiguration.java create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/context/RequestContext.java create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/context/RequestContextHolder.java rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/controller/AgentController.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/controller/AgentExceptionHandler.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/controller/CredentialMaskingResponseAdvice.java (87%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/controller/SecretController.java (94%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/controller/SkillController.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/controller/WorkerController.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTask.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskConfig.java (60%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/credentials/CredentialAwareMcpService.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/credentials/CredentialResolutionService.java (98%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/credentials/ExecutionTokenService.java (100%) create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/KnownProviderEnvVars.java rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/AgentConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/AgentExecutionDetail.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/AgentExecutionSummary.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/AgentRun.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/AgentSSEEvent.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/AgentSummary.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/CallbackConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/CliConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/CodeExecutionConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/CompileResponse.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/CreateTrackingWorkflowRequest.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/CreateTrackingWorkflowResponse.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/GuardrailConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/HandoffConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/InjectTaskRequest.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/InjectTaskResponse.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/InspectPlanRequest.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/MemoryConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/OutputTypeConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/PrefillToolCallConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/PromptTemplateRef.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/StartRequest.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/StartResponse.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/TaskListResponse.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/TerminationConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/ThinkingConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/ToolConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/WorkerRef.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/credentials/CredentialMeta.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/credentials/ResolveRequest.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/credentials/ResolveResponse.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/skill/SkillDeployRequest.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/skill/SkillDetail.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/skill/SkillFileContent.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/skill/SkillFileEntry.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/model/skill/SkillSummary.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/normalizer/AgentConfigNormalizer.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/normalizer/ClaudeAgentSdkNormalizer.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/normalizer/GoogleADKNormalizer.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/normalizer/LangChainNormalizer.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/normalizer/LangGraphNormalizer.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/normalizer/NormalizerRegistry.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/normalizer/OpenAINormalizer.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/normalizer/SkillNormalizer.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/normalizer/VercelAINormalizer.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/AgentDagService.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/AgentEventListener.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/AgentHumanTask.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/AgentHumanTaskConfig.java (51%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/AgentService.java (94%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/AgentStreamRegistry.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/ListApiToolsTask.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/ListApiToolsTaskConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/PlanAndCompileTask.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/PlanAndCompileTaskConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/PlannerContextFetchTask.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/PlannerContextFetchTaskConfig.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/service/SkillRegistryService.java (78%) rename server/{src/main/java/dev/agentspan/runtime/credentials => conductor-agentspan/src/main/java/dev/agentspan/runtime/spi}/CredentialStoreProvider.java (74%) create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/spi/SecretOutputMasker.java create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/spi/SkillMetadataDAO.java rename server/{src/main/java/dev/agentspan/runtime/service/skill => conductor-agentspan/src/main/java/dev/agentspan/runtime/spi}/SkillPackageStore.java (89%) rename server/{src/main/java/dev/agentspan/runtime/service/skill => conductor-agentspan/src/main/java/dev/agentspan/runtime/spi}/StoredSkillPackage.java (82%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/tasks/Join.java (97%) create mode 100644 server/conductor-agentspan/src/main/java/dev/agentspan/runtime/util/EmbeddedMode.java rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/util/JavaScriptBuilder.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/util/ModelContextWindows.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/util/ModelParser.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/util/ProviderValidator.java (56%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/util/SafeConditionInterpreter.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/util/SafeConditionParseException.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/util/SchemaSubsetValidator.java (100%) rename server/{ => conductor-agentspan}/src/main/java/dev/agentspan/runtime/util/WorkflowTaskUtils.java (100%) create mode 100644 server/conductor-agentspan/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 server/docs/agentspan-as-a-library.md delete mode 100644 server/src/main/java/dev/agentspan/runtime/auth/ApiKeyRepository.java delete mode 100644 server/src/main/java/dev/agentspan/runtime/auth/AuthFilter.java delete mode 100644 server/src/main/java/dev/agentspan/runtime/auth/AuthProperties.java delete mode 100644 server/src/main/java/dev/agentspan/runtime/auth/AuthUserSeeder.java delete mode 100644 server/src/main/java/dev/agentspan/runtime/auth/RequestContext.java delete mode 100644 server/src/main/java/dev/agentspan/runtime/auth/RequestContextHolder.java delete mode 100644 server/src/main/java/dev/agentspan/runtime/auth/User.java delete mode 100644 server/src/main/java/dev/agentspan/runtime/auth/UserRepository.java delete mode 100644 server/src/main/java/dev/agentspan/runtime/controller/AuthController.java delete mode 100644 server/src/main/java/dev/agentspan/runtime/credentials/CredentialOutputMasker.java delete mode 100644 server/src/test/java/dev/agentspan/runtime/auth/ApiKeyRepositoryTest.java delete mode 100644 server/src/test/java/dev/agentspan/runtime/auth/AuthFilterTest.java delete mode 100644 server/src/test/java/dev/agentspan/runtime/auth/AuthUserSeederTest.java delete mode 100644 server/src/test/java/dev/agentspan/runtime/auth/UserRepositoryTest.java delete mode 100644 server/src/test/java/dev/agentspan/runtime/controller/AuthControllerTest.java delete mode 100644 ui/src/pages/secrets/__tests__/LoginDialog.test.tsx delete mode 100644 ui/src/pages/secrets/__tests__/useSecretAuth.test.ts delete mode 100644 ui/src/pages/secrets/components/LoginDialog.tsx delete mode 100644 ui/src/pages/secrets/hooks/useSecretAuth.ts diff --git a/.github/workflows/ci-csharp-sdk-e2e.yml b/.github/workflows/ci-csharp-sdk-e2e.yml index bf6ca013c..0652a39de 100644 --- a/.github/workflows/ci-csharp-sdk-e2e.yml +++ b/.github/workflows/ci-csharp-sdk-e2e.yml @@ -37,7 +37,7 @@ jobs: - name: Start server run: | - java -jar server/build/libs/agentspan-runtime.jar --server.port=6767 & + java -jar server/conductor-agentspan-server/build/libs/agentspan-runtime.jar --server.port=6767 & echo "SERVER_PID=$!" >> $GITHUB_ENV for i in $(seq 1 30); do if curl -sf http://localhost:6767/health > /dev/null 2>&1; then diff --git a/.github/workflows/ci-java-sdk-e2e.yml b/.github/workflows/ci-java-sdk-e2e.yml index 58c5802df..267a0f47a 100644 --- a/.github/workflows/ci-java-sdk-e2e.yml +++ b/.github/workflows/ci-java-sdk-e2e.yml @@ -32,7 +32,7 @@ jobs: - name: Start server run: | - java -jar server/build/libs/agentspan-runtime.jar --server.port=6767 & + java -jar server/conductor-agentspan-server/build/libs/agentspan-runtime.jar --server.port=6767 & echo "SERVER_PID=$!" >> $GITHUB_ENV for i in $(seq 1 30); do if curl -sf http://localhost:6767/health > /dev/null 2>&1; then diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf5a9a914..c2ca35eb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -200,7 +200,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: server-jar - path: server/build/libs/agentspan-runtime.jar + path: server/conductor-agentspan-server/build/libs/agentspan-runtime.jar retention-days: 7 # ── Python E2E Tests ─────────────────────────────────────────────── @@ -229,7 +229,7 @@ jobs: uses: actions/download-artifact@v4 with: name: server-jar - path: server/build/libs/ + path: server/conductor-agentspan-server/build/libs/ - name: Build CLI working-directory: cli @@ -251,7 +251,7 @@ jobs: - name: Start server run: | - java -jar server/build/libs/agentspan-runtime.jar --server.port=6767 & + java -jar server/conductor-agentspan-server/build/libs/agentspan-runtime.jar --server.port=6767 & for i in $(seq 1 30); do curl -sf http://localhost:6767/health && break; sleep 2; done - name: Run Python e2e suites 1-13 @@ -259,7 +259,7 @@ jobs: run: | uv run pytest e2e/ -v --tb=short \ --junitxml=../../e2e-results/junit.xml \ - -n 1 + -n 3 --dist=loadgroup - name: Generate Python HTML report if: always() @@ -302,7 +302,7 @@ jobs: uses: actions/download-artifact@v4 with: name: server-jar - path: server/build/libs/ + path: server/conductor-agentspan-server/build/libs/ - name: Build CLI working-directory: cli @@ -318,7 +318,7 @@ jobs: - name: Start server run: | - java -jar server/build/libs/agentspan-runtime.jar --server.port=6767 & + java -jar server/conductor-agentspan-server/build/libs/agentspan-runtime.jar --server.port=6767 & for i in $(seq 1 30); do curl -sf http://localhost:6767/health && break; sleep 2; done - name: Install TypeScript SDK @@ -365,7 +365,7 @@ jobs: uses: actions/download-artifact@v4 with: name: server-jar - path: server/build/libs/ + path: server/conductor-agentspan-server/build/libs/ - name: Install mcp-testkit run: pip install mcp-testkit @@ -377,7 +377,7 @@ jobs: - name: Start server run: | - java -jar server/build/libs/agentspan-runtime.jar --server.port=6767 & + java -jar server/conductor-agentspan-server/build/libs/agentspan-runtime.jar --server.port=6767 & for i in $(seq 1 30); do curl -sf http://localhost:6767/health && break; sleep 2; done - name: Run Java e2e suites @@ -422,11 +422,11 @@ jobs: uses: actions/download-artifact@v4 with: name: server-jar - path: server/build/libs/ + path: server/conductor-agentspan-server/build/libs/ - name: Start server run: | - java -jar server/build/libs/agentspan-runtime.jar --server.port=6767 & + java -jar server/conductor-agentspan-server/build/libs/agentspan-runtime.jar --server.port=6767 & for i in $(seq 1 30); do curl -sf http://localhost:6767/health && break; sleep 2; done - name: Run C# e2e suites diff --git a/.github/workflows/release-server-maven.yml b/.github/workflows/release-server-maven.yml new file mode 100644 index 000000000..bbcd488d2 --- /dev/null +++ b/.github/workflows/release-server-maven.yml @@ -0,0 +1,50 @@ +name: Publish Server to Maven Central + +on: + workflow_dispatch: + inputs: + version: + description: "Version (e.g. 0.2.0)" + required: true + type: string + +permissions: + contents: read + +jobs: + publish-maven-central: + runs-on: ubuntu-latest + environment: maven-central + defaults: + run: + working-directory: server + + steps: + - uses: actions/checkout@v4 + + - name: Set up Zulu JDK 21 + uses: actions/setup-java@v5 + with: + distribution: "zulu" + java-version: "21" + + - name: Determine version + id: version + run: | + VERSION="${{ inputs.version }}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Publishing version: ${VERSION}" + + # Publishes both modules (conductor-agentspan, conductor-agentspan-server) + # under org.conductoross.conductor. The runnable fat jar is released + # separately via the S3/GitHub workflow, not to Maven Central. + - name: Publish to Maven Central + run: | + ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache \ + -Pversion=${{ steps.version.outputs.version }} + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} diff --git a/AGENTS.md b/AGENTS.md index 47c314dfd..b5198adea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,7 +48,7 @@ When `run(agent, prompt)` is called: | `src/agentspan/agents/termination.py` | `TerminationCondition` and composable subclasses (`&`, `|` operators) | | `src/agentspan/agents/handoff.py` | `HandoffCondition`, `OnToolResult`, `OnTextMention`, `OnCondition` | | `src/agentspan/agents/code_executor.py` | `CodeExecutor` — Local, Docker, Jupyter, Serverless | -| `src/agentspan/agents/ext.py` | `UserProxyAgent`, `GPTAssistantAgent` | +| `src/agentspan/agents/ext.py` | `GPTAssistantAgent` | | `src/agentspan/agents/tracing.py` | Optional OpenTelemetry integration | | `src/agentspan/agents/__init__.py` | Public API surface — all exports | | `src/agentspan/agents/compiler/agent_compiler.py` | Single agent compilation (DoWhile loops, tool dispatch) | diff --git a/README.md b/README.md index 3a03c811a..9133d2892 100644 --- a/README.md +++ b/README.md @@ -579,7 +579,6 @@ Execution order: `on_agent_start` → (`on_model_start` → LLM → `on_model_en | [`24_code_execution.py`](sdk/python/examples/24_code_execution.py) | Code execution sandboxes | | [`25_semantic_memory.py`](sdk/python/examples/25_semantic_memory.py) | Long-term memory with retrieval | | [`26_opentelemetry_tracing.py`](sdk/python/examples/26_opentelemetry_tracing.py) | OpenTelemetry spans | -| [`27_user_proxy_agent.py`](sdk/python/examples/27_user_proxy_agent.py) | Interactive conversations | | [`28_gpt_assistant_agent.py`](sdk/python/examples/28_gpt_assistant_agent.py) | OpenAI Assistants API wrapper | | [`29_agent_introductions.py`](sdk/python/examples/29_agent_introductions.py) | Agents introduce themselves | | [`30_multimodal_agent.py`](sdk/python/examples/30_multimodal_agent.py) | Vision model analysis | diff --git a/cli/auth/auth0.go b/cli/auth/auth0.go new file mode 100644 index 000000000..8dd20be38 --- /dev/null +++ b/cli/auth/auth0.go @@ -0,0 +1,227 @@ +// Copyright (c) 2025 AgentSpan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +// Package auth implements the Auth0 OAuth Device Authorization Grant (RFC 8628) +// for browser-based CLI login. This mirrors how the orkes-conductor UI authenticates: +// it obtains an Auth0-issued JWT and sends it to the backend as the X-Authorization +// header — there is no orkes-side token exchange. The device flow delegates the actual +// login to Auth0's hosted page, so it supports whatever the tenant allows +// (username/password, Google, SSO, MFA, ...). +package auth + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "time" +) + +// DefaultScope requests an OIDC token plus a refresh token. No audience is requested, +// matching the UI (which relies on the Auth0 tenant's default audience / ID token). +const DefaultScope = "openid profile email offline_access" + +// DeviceCode is the response from POST /oauth/device/code. +type DeviceCode struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURI string `json:"verification_uri"` + VerificationURIComplete string `json:"verification_uri_complete"` + ExpiresIn int `json:"expires_in"` + Interval int `json:"interval"` +} + +// Token is the response from POST /oauth/token. +type Token struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` +} + +// Auth0Config is the subset of the server's window.authConfig the CLI needs. +type Auth0Config struct { + Domain string + ClientID string + UseIDToken bool +} + +// RequestDeviceCode starts the device authorization flow. +func RequestDeviceCode(domain, clientID, scope string) (*DeviceCode, error) { + if scope == "" { + scope = DefaultScope + } + form := url.Values{"client_id": {clientID}, "scope": {scope}} + resp, err := http.PostForm(authURL(domain, "/oauth/device/code"), form) + if err != nil { + return nil, fmt.Errorf("request device code: %w", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("device code request failed (HTTP %d): %s", resp.StatusCode, string(body)) + } + var dc DeviceCode + if err := json.Unmarshal(body, &dc); err != nil { + return nil, fmt.Errorf("parse device code: %w", err) + } + if dc.Interval <= 0 { + dc.Interval = 5 + } + return &dc, nil +} + +// PollForToken polls the token endpoint until the user completes login in the browser, +// the code expires, or login is denied. Honors authorization_pending and slow_down. +func PollForToken(domain, clientID string, dc *DeviceCode) (*Token, error) { + deadline := time.Now().Add(time.Duration(dc.ExpiresIn) * time.Second) + interval := time.Duration(dc.Interval) * time.Second + for { + if time.Now().After(deadline) { + return nil, fmt.Errorf("device code expired before login completed") + } + time.Sleep(interval) + + form := url.Values{ + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + "device_code": {dc.DeviceCode}, + "client_id": {clientID}, + } + resp, err := http.PostForm(authURL(domain, "/oauth/token"), form) + if err != nil { + return nil, fmt.Errorf("poll token: %w", err) + } + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + var tok Token + if err := json.Unmarshal(body, &tok); err != nil { + return nil, fmt.Errorf("parse token: %w", err) + } + return &tok, nil + } + + var e struct { + Error string `json:"error"` + } + _ = json.Unmarshal(body, &e) + switch e.Error { + case "authorization_pending": + // keep waiting + case "slow_down": + interval += 5 * time.Second + case "expired_token": + return nil, fmt.Errorf("device code expired before login completed") + case "access_denied": + return nil, fmt.Errorf("login was denied") + default: + return nil, fmt.Errorf("token poll failed (HTTP %d): %s", resp.StatusCode, string(body)) + } + } +} + +// Refresh exchanges a refresh token for a fresh access/ID token. +func Refresh(domain, clientID, refreshToken string) (*Token, error) { + form := url.Values{ + "grant_type": {"refresh_token"}, + "client_id": {clientID}, + "refresh_token": {refreshToken}, + } + resp, err := http.PostForm(authURL(domain, "/oauth/token"), form) + if err != nil { + return nil, fmt.Errorf("refresh token: %w", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("refresh failed (HTTP %d): %s", resp.StatusCode, string(body)) + } + var tok Token + if err := json.Unmarshal(body, &tok); err != nil { + return nil, fmt.Errorf("parse refreshed token: %w", err) + } + return &tok, nil +} + +// DiscoverConfig fetches the server's /context.js and extracts the Auth0 config — the +// same runtime config the UI consumes, with the UI's exact resolution order +// (auth0-config.ts): authConfig.domain || auth0Identifiers.domain, same for clientId. +// Handles the real orkes serialization (quoted keys, e.g. {"clientId" : "..."}) as well +// as bare-key JS object literals. +func DiscoverConfig(serverBaseURL string) (*Auth0Config, error) { + base := strings.TrimRight(serverBaseURL, "/") + resp, err := http.Get(base + "/context.js") + if err != nil { + return nil, fmt.Errorf("fetch /context.js: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("/context.js returned HTTP %d", resp.StatusCode) + } + body, _ := io.ReadAll(resp.Body) + js := string(body) + + authBlock := sliceBlock(js, "window.authConfig") + idsBlock := sliceBlock(js, "window.auth0Identifiers") + + domain := extract(authBlock, "domain") + if domain == "" { + domain = extract(idsBlock, "domain") + } + clientID := extract(authBlock, "clientId") + if clientID == "" { + clientID = extract(idsBlock, "clientId") + } + + cfg := &Auth0Config{ + Domain: domain, + ClientID: clientID, + UseIDToken: useIDTokenTrueRe.MatchString(authBlock), + } + if cfg.Domain == "" || cfg.ClientID == "" { + return nil, fmt.Errorf("server has no Auth0 config in /context.js (is auth enabled?)") + } + return cfg, nil +} + +var useIDTokenTrueRe = regexp.MustCompile(`["']?useIdToken["']?\s*:\s*true`) + +// sliceBlock returns the {...} object literal assigned at `marker` (e.g. "window.authConfig"), +// or "" when absent. The blocks served by orkes/the UI are flat objects, so the first `}` +// after the marker closes the block. +func sliceBlock(js, marker string) string { + start := strings.Index(js, marker) + if start < 0 { + return "" + } + rest := js[start:] + end := strings.Index(rest, "}") + if end < 0 { + return rest + } + return rest[:end+1] +} + +// extract pulls a string value for `key` from a JS/JSON object literal, tolerating both +// quoted and bare keys and arbitrary spacing around the colon. +func extract(js, key string) string { + m := regexp.MustCompile(`["']?` + key + `["']?\s*:\s*["']([^"']+)["']`).FindStringSubmatch(js) + if len(m) == 2 { + return m[1] + } + return "" +} + +func authURL(domain, path string) string { + domain = strings.TrimRight(domain, "/") + if !strings.HasPrefix(domain, "http") { + domain = "https://" + domain + } + return domain + path +} diff --git a/cli/auth/auth0_test.go b/cli/auth/auth0_test.go new file mode 100644 index 000000000..67da174e6 --- /dev/null +++ b/cli/auth/auth0_test.go @@ -0,0 +1,181 @@ +package auth + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +// TestDeviceFlow drives RequestDeviceCode + PollForToken against a real local HTTP +// server emulating Auth0's device endpoints, including one authorization_pending poll. +func TestDeviceFlow(t *testing.T) { + polls := 0 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/oauth/device/code": + _ = json.NewEncoder(w).Encode(map[string]any{ + "device_code": "dev-123", + "user_code": "WXYZ-1234", + "verification_uri": "https://example/activate", + "verification_uri_complete": "https://example/activate?user_code=WXYZ-1234", + "expires_in": 300, + "interval": 1, + }) + case "/oauth/token": + polls++ + if polls < 2 { + w.WriteHeader(http.StatusForbidden) + _ = json.NewEncoder(w).Encode(map[string]string{"error": "authorization_pending"}) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "access_token": "access-xyz", + "id_token": "id-xyz", + "refresh_token": "refresh-xyz", + "expires_in": 3600, + "token_type": "Bearer", + }) + default: + http.NotFound(w, r) + } + })) + defer srv.Close() + + dc, err := RequestDeviceCode(srv.URL, "client-id", "") + if err != nil { + t.Fatalf("RequestDeviceCode: %v", err) + } + if dc.UserCode != "WXYZ-1234" { + t.Errorf("user code = %q", dc.UserCode) + } + + tok, err := PollForToken(srv.URL, "client-id", dc) + if err != nil { + t.Fatalf("PollForToken: %v", err) + } + if tok.AccessToken != "access-xyz" || tok.RefreshToken != "refresh-xyz" { + t.Errorf("unexpected token: %+v", tok) + } + if polls < 2 { + t.Errorf("expected at least 2 polls (one pending), got %d", polls) + } +} + +func TestRefresh(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "bad form", http.StatusBadRequest) + return + } + if r.FormValue("grant_type") != "refresh_token" || r.FormValue("refresh_token") != "rt" { + http.Error(w, "bad refresh", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"access_token": "new-access", "expires_in": 3600}) + })) + defer srv.Close() + + tok, err := Refresh(srv.URL, "client-id", "rt") + if err != nil { + t.Fatalf("Refresh: %v", err) + } + if tok.AccessToken != "new-access" { + t.Errorf("refreshed access token = %q", tok.AccessToken) + } +} + +func TestDiscoverConfig(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/context.js" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/javascript") + _, _ = w.Write([]byte(`window.authConfig = { type: "auth0", domain: "tenant.us.auth0.com", clientId: "abc123", useIdToken: true };`)) + })) + defer srv.Close() + + cfg, err := DiscoverConfig(srv.URL) + if err != nil { + t.Fatalf("DiscoverConfig: %v", err) + } + if cfg.Domain != "tenant.us.auth0.com" || cfg.ClientID != "abc123" || !cfg.UseIDToken { + t.Errorf("unexpected config: %+v", cfg) + } +} + +// TestDiscoverConfigRealOrkesFormat uses the exact serialization a real orkes server +// emits — quoted keys with spaces around the colon, plus the window.conductor flags +// block and the auth0Identifiers fallback block (regression: bare-key-only regex). +func TestDiscoverConfigRealOrkesFormat(t *testing.T) { + body := `window.conductor = { + "ENABLE_METRICS_DASHBOARD" : false, + "MULTITENANCY_TYPE" : "none" +}; + +window.authConfig = { + "useIdToken" : false, + "clientId" : "s4HLdVbnaJMGvPSgx2YLpynfJlW7GV2e", + "domain" : "auth.orkes.io", + "type" : "auth0" +}; + +window.auth0Identifiers = { + "clientId" : "s4HLdVbnaJMGvPSgx2YLpynfJlW7GV2e", + "domain" : "auth.orkes.io" +};` + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/context.js" { + http.NotFound(w, r) + return + } + _, _ = w.Write([]byte(body)) + })) + defer srv.Close() + + cfg, err := DiscoverConfig(srv.URL) + if err != nil { + t.Fatalf("DiscoverConfig: %v", err) + } + if cfg.Domain != "auth.orkes.io" || cfg.ClientID != "s4HLdVbnaJMGvPSgx2YLpynfJlW7GV2e" { + t.Errorf("unexpected config: %+v", cfg) + } + if cfg.UseIDToken { + t.Error("useIdToken=false in authConfig but parsed true") + } +} + +// TestDiscoverConfigIdentifiersFallback mirrors the UI's auth0-config.ts fallback: +// authConfig missing -> values from window.auth0Identifiers. +func TestDiscoverConfigIdentifiersFallback(t *testing.T) { + body := `window.auth0Identifiers = { + "clientId" : "cid-fallback", + "domain" : "tenant.eu.auth0.com" +};` + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(body)) + })) + defer srv.Close() + + cfg, err := DiscoverConfig(srv.URL) + if err != nil { + t.Fatalf("DiscoverConfig: %v", err) + } + if cfg.Domain != "tenant.eu.auth0.com" || cfg.ClientID != "cid-fallback" { + t.Errorf("unexpected config: %+v", cfg) + } +} + +func TestDiscoverConfigNoAuth(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`window.conductor = { stack: "test" };`)) + })) + defer srv.Close() + + if _, err := DiscoverConfig(srv.URL); err == nil { + t.Fatal("expected error when /context.js has no authConfig") + } +} diff --git a/cli/auth/orkes.go b/cli/auth/orkes.go new file mode 100644 index 000000000..466c65dca --- /dev/null +++ b/cli/auth/orkes.go @@ -0,0 +1,60 @@ +// Copyright (c) 2025 AgentSpan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package auth + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +// MintOrkesToken exchanges an orkes application access key (keyId/keySecret) for a JWT +// via POST {server}/api/token — orkes' internal/service-account auth path (no external IdP). +// Returns the JWT and its expiry (unix seconds, decoded from the token's exp claim). +func MintOrkesToken(serverBase, keyID, keySecret string) (token string, expiresAt int64, err error) { + base := strings.TrimRight(serverBase, "/") + body, _ := json.Marshal(map[string]string{"keyId": keyID, "keySecret": keySecret}) + resp, err := http.Post(base+"/api/token", "application/json", bytes.NewReader(body)) + if err != nil { + return "", 0, fmt.Errorf("mint token: %w", err) + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return "", 0, fmt.Errorf("token request failed (HTTP %d): %s", resp.StatusCode, strings.TrimSpace(string(b))) + } + var r struct { + Token string `json:"token"` + } + if json.Unmarshal(b, &r) != nil || r.Token == "" { + return "", 0, fmt.Errorf("no token in response: %s", strings.TrimSpace(string(b))) + } + return r.Token, TokenExp(r.Token), nil +} + +// TokenExp decodes the `exp` (unix seconds) claim from a JWT without verifying the signature. +// Returns 0 when the token is opaque or has no exp. +func TokenExp(jwt string) int64 { + parts := strings.Split(jwt, ".") + if len(parts) < 2 { + return 0 + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + if payload, err = base64.URLEncoding.DecodeString(parts[1]); err != nil { + return 0 + } + } + var claims struct { + Exp int64 `json:"exp"` + } + if json.Unmarshal(payload, &claims) != nil { + return 0 + } + return claims.Exp +} diff --git a/cli/auth/orkes_test.go b/cli/auth/orkes_test.go new file mode 100644 index 000000000..0bbcc9f43 --- /dev/null +++ b/cli/auth/orkes_test.go @@ -0,0 +1,62 @@ +package auth + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestMintOrkesToken(t *testing.T) { + header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS512","typ":"JWT"}`)) + payload := base64.RawURLEncoding.EncodeToString([]byte(`{"exp":4102444800,"orkes_conductor_token":true}`)) + jwt := header + "." + payload + ".sig" + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/token" || r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + var body map[string]string + _ = json.NewDecoder(r.Body).Decode(&body) + if body["keyId"] != "kid" || body["keySecret"] != "ksecret" { + http.Error(w, "bad creds", http.StatusUnauthorized) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{"token": jwt}) + })) + defer srv.Close() + + token, exp, err := MintOrkesToken(srv.URL, "kid", "ksecret") + if err != nil { + t.Fatalf("MintOrkesToken: %v", err) + } + if token != jwt { + t.Errorf("token mismatch") + } + if exp != 4102444800 { + t.Errorf("exp = %d, want 4102444800", exp) + } +} + +func TestMintOrkesTokenBadCreds(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "unauthorized", http.StatusUnauthorized) + })) + defer srv.Close() + if _, _, err := MintOrkesToken(srv.URL, "x", "y"); err == nil { + t.Fatal("expected error on 401") + } +} + +func TestTokenExp(t *testing.T) { + payload := base64.RawURLEncoding.EncodeToString([]byte(`{"exp":1700000000}`)) + if got := TokenExp("h." + payload + ".s"); got != 1700000000 { + t.Errorf("TokenExp = %d, want 1700000000", got) + } + if got := TokenExp("opaque-token"); got != 0 { + t.Errorf("TokenExp(opaque) = %d, want 0", got) + } +} diff --git a/cli/auth/pkce.go b/cli/auth/pkce.go new file mode 100644 index 000000000..80994b945 --- /dev/null +++ b/cli/auth/pkce.go @@ -0,0 +1,174 @@ +// Copyright (c) 2025 AgentSpan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package auth + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// PKCE implements the OAuth 2.0 Authorization Code flow with PKCE (RFC 7636) over a +// loopback redirect (RFC 8252 §7.3) — the browser-based login a native CLI uses when the +// Device Authorization grant is not enabled on the client. It reuses the SAME public +// clientId the orkes UI uses (authorization_code + PKCE is what the UI itself runs); +// the only requirement is that the loopback redirect URI is in the Auth0 application's +// Allowed Callback URLs (the local UI origin, e.g. http://localhost:5001, already is). + +// GeneratePKCE returns a code_verifier and its S256 code_challenge. +func GeneratePKCE() (verifier, challenge string, err error) { + buf := make([]byte, 32) + if _, err = rand.Read(buf); err != nil { + return "", "", fmt.Errorf("generate PKCE verifier: %w", err) + } + verifier = base64.RawURLEncoding.EncodeToString(buf) + sum := sha256.Sum256([]byte(verifier)) + challenge = base64.RawURLEncoding.EncodeToString(sum[:]) + return verifier, challenge, nil +} + +// RandomState returns a random opaque state value for CSRF protection. +func RandomState() (string, error) { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(buf), nil +} + +// BuildAuthorizeURL constructs the Auth0 /authorize URL for the code+PKCE flow. +func BuildAuthorizeURL(domain, clientID, redirectURI, scope, state, challenge string) string { + if scope == "" { + scope = DefaultScope + } + q := url.Values{ + "client_id": {clientID}, + "response_type": {"code"}, + "redirect_uri": {redirectURI}, + "scope": {scope}, + "state": {state}, + "code_challenge": {challenge}, + "code_challenge_method": {"S256"}, + } + return authURL(domain, "/authorize") + "?" + q.Encode() +} + +// CodeCapture is a bound loopback listener awaiting the OAuth redirect. +type CodeCapture struct { + srv *http.Server + resCh chan captureResult +} + +type captureResult struct { + code string + err error +} + +// StartCodeCapture binds the loopback host:port of redirectURI immediately — failing fast +// if the port is busy (e.g. the UI dev server holds it) BEFORE any browser is opened — and +// starts serving the callback. Call Wait to block for the code. +func StartCodeCapture(redirectURI, expectedState string) (*CodeCapture, error) { + u, err := url.Parse(redirectURI) + if err != nil { + return nil, fmt.Errorf("parse redirect uri: %w", err) + } + if u.Port() == "" { + return nil, fmt.Errorf("redirect uri must include an explicit port (got %q)", redirectURI) + } + path := u.Path + if path == "" { + path = "/" + } + + ln, err := net.Listen("tcp", u.Host) + if err != nil { + return nil, fmt.Errorf( + "cannot bind %s — is something (the UI dev server?) running on that port? "+ + "Stop it during login or pass a different whitelisted --redirect-uri (%w)", u.Host, err) + } + + resCh := make(chan captureResult, 1) + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Accept the callback on the redirect path (or root) only. + if r.URL.Path != path && r.URL.Path != "/" { + http.NotFound(w, r) + return + } + q := r.URL.Query() + if e := q.Get("error"); e != "" { + http.Error(w, "Login failed: "+e, http.StatusBadRequest) + resCh <- captureResult{err: fmt.Errorf("authorization failed: %s (%s)", e, q.Get("error_description"))} + return + } + code := q.Get("code") + if code == "" { + // Not the OAuth callback (e.g. favicon) — ignore. + http.NotFound(w, r) + return + } + if q.Get("state") != expectedState { + http.Error(w, "State mismatch", http.StatusBadRequest) + resCh <- captureResult{err: fmt.Errorf("state mismatch in callback")} + return + } + w.Header().Set("Content-Type", "text/html") + _, _ = io.WriteString(w, "

    Login complete.

    You can close this tab and return to the terminal.") + resCh <- captureResult{code: code} + }) + + srv := &http.Server{Handler: mux} + go func() { _ = srv.Serve(ln) }() + return &CodeCapture{srv: srv, resCh: resCh}, nil +} + +// Wait blocks until the redirect delivers a code, an OAuth error arrives, or timeout. +// The listener is shut down before returning. +func (c *CodeCapture) Wait(timeout time.Duration) (string, error) { + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + _ = c.srv.Shutdown(ctx) + }() + select { + case r := <-c.resCh: + return r.code, r.err + case <-time.After(timeout): + return "", fmt.Errorf("timed out waiting for the browser login (after %s)", timeout) + } +} + +// ExchangeCode swaps an authorization code + PKCE verifier for tokens (public client). +func ExchangeCode(domain, clientID, code, verifier, redirectURI string) (*Token, error) { + form := url.Values{ + "grant_type": {"authorization_code"}, + "client_id": {clientID}, + "code": {code}, + "code_verifier": {verifier}, + "redirect_uri": {redirectURI}, + } + resp, err := http.PostForm(authURL(domain, "/oauth/token"), form) + if err != nil { + return nil, fmt.Errorf("exchange code: %w", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("code exchange failed (HTTP %d): %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + var tok Token + if err := json.Unmarshal(body, &tok); err != nil { + return nil, fmt.Errorf("parse token: %w", err) + } + return &tok, nil +} diff --git a/cli/auth/pkce_test.go b/cli/auth/pkce_test.go new file mode 100644 index 000000000..3c1d07398 --- /dev/null +++ b/cli/auth/pkce_test.go @@ -0,0 +1,159 @@ +package auth + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" +) + +func TestGeneratePKCE(t *testing.T) { + verifier, challenge, err := GeneratePKCE() + if err != nil { + t.Fatalf("GeneratePKCE: %v", err) + } + sum := sha256.Sum256([]byte(verifier)) + want := base64.RawURLEncoding.EncodeToString(sum[:]) + if challenge != want { + t.Errorf("challenge mismatch: got %q want %q", challenge, want) + } + if len(verifier) < 43 { // 32 bytes base64url = 43 chars, RFC 7636 minimum + t.Errorf("verifier too short: %d", len(verifier)) + } +} + +func TestBuildAuthorizeURL(t *testing.T) { + u, err := url.Parse(BuildAuthorizeURL("tenant.auth0.com", "cid", "http://localhost:5001", "", "st8", "ch4ll")) + if err != nil { + t.Fatalf("parse: %v", err) + } + if u.Host != "tenant.auth0.com" || u.Path != "/authorize" { + t.Errorf("unexpected url: %s", u) + } + q := u.Query() + for k, want := range map[string]string{ + "client_id": "cid", + "response_type": "code", + "redirect_uri": "http://localhost:5001", + "state": "st8", + "code_challenge": "ch4ll", + "code_challenge_method": "S256", + "scope": DefaultScope, + } { + if got := q.Get(k); got != want { + t.Errorf("%s = %q, want %q", k, got, want) + } + } +} + +// freePort grabs an ephemeral port and releases it for the capture server to rebind. +func freePort(t *testing.T) int { + t.Helper() + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + port := ln.Addr().(*net.TCPAddr).Port + _ = ln.Close() + return port +} + +func TestCodeCapture(t *testing.T) { + port := freePort(t) + redirect := fmt.Sprintf("http://127.0.0.1:%d", port) + + capture, err := StartCodeCapture(redirect, "state-1") + if err != nil { + t.Fatalf("StartCodeCapture: %v", err) + } + + // Simulate the browser redirect. + go func() { + resp, gerr := http.Get(fmt.Sprintf("%s/?code=auth-code-42&state=state-1", redirect)) + if gerr == nil { + resp.Body.Close() + } + }() + + code, err := capture.Wait(10 * time.Second) + if err != nil { + t.Fatalf("Wait: %v", err) + } + if code != "auth-code-42" { + t.Errorf("code = %q", code) + } +} + +func TestCodeCaptureStateMismatch(t *testing.T) { + port := freePort(t) + redirect := fmt.Sprintf("http://127.0.0.1:%d", port) + + capture, err := StartCodeCapture(redirect, "expected") + if err != nil { + t.Fatalf("StartCodeCapture: %v", err) + } + + go func() { + resp, gerr := http.Get(fmt.Sprintf("%s/?code=c&state=WRONG", redirect)) + if gerr == nil { + resp.Body.Close() + } + }() + + if _, err := capture.Wait(10 * time.Second); err == nil { + t.Fatal("expected state-mismatch error") + } +} + +func TestStartCodeCapturePortBusy(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + defer ln.Close() + busy := fmt.Sprintf("http://127.0.0.1:%d", ln.Addr().(*net.TCPAddr).Port) + + // Bind must fail immediately — BEFORE any browser would be opened. + if _, err := StartCodeCapture(busy, "s"); err == nil { + t.Fatal("expected bind error on busy port") + } +} + +func TestExchangeCode(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/oauth/token" { + http.NotFound(w, r) + return + } + if err := r.ParseForm(); err != nil { + http.Error(w, "bad form", http.StatusBadRequest) + return + } + if r.FormValue("grant_type") != "authorization_code" || + r.FormValue("code") != "the-code" || + r.FormValue("code_verifier") != "the-verifier" || + r.FormValue("redirect_uri") != "http://localhost:5001" { + http.Error(w, "bad exchange params", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "access_token": "at", "id_token": "idt", "refresh_token": "rt", "expires_in": 3600, + }) + })) + defer srv.Close() + + tok, err := ExchangeCode(srv.URL, "cid", "the-code", "the-verifier", "http://localhost:5001") + if err != nil { + t.Fatalf("ExchangeCode: %v", err) + } + if tok.AccessToken != "at" || tok.RefreshToken != "rt" { + t.Errorf("unexpected token: %+v", tok) + } +} diff --git a/cli/client/client.go b/cli/client/client.go index 37b0827a7..7f23eee58 100644 --- a/cli/client/client.go +++ b/cli/client/client.go @@ -15,6 +15,7 @@ import ( "strings" "time" + "github.com/agentspan-ai/agentspan/cli/auth" "github.com/agentspan-ai/agentspan/cli/config" ) @@ -22,14 +23,61 @@ type Client struct { baseURL string httpClient *http.Client apiKey string + authToken string // Auth0 JWT sent as X-Authorization (from ~/.agentspan/token) } func New(cfg *config.Config) *Client { - return &Client{ + c := &Client{ baseURL: strings.TrimRight(cfg.ServerURL, "/"), httpClient: &http.Client{Timeout: 30 * time.Second}, apiKey: cfg.APIKey, } + c.authToken = resolveAuthToken() + return c +} + +// resolveAuthToken loads the stored login token, refreshing it via Auth0 if expired, +// and returns the JWT to send as X-Authorization. Empty when not logged in. +func resolveAuthToken() string { + t, err := config.LoadToken() + if err != nil || t == nil { + return "" + } + if t.Expired() { + switch { + case t.RefreshToken != "" && t.Auth0Domain != "" && t.ClientID != "": + // Auth0 device-flow token: refresh with the refresh token. + if nt, rerr := auth.Refresh(t.Auth0Domain, t.ClientID, t.RefreshToken); rerr == nil { + t.AccessToken = nt.AccessToken + if nt.IDToken != "" { + t.IDToken = nt.IDToken + } + if nt.RefreshToken != "" { + t.RefreshToken = nt.RefreshToken + } + t.ExpiresAt = time.Now().Add(time.Duration(nt.ExpiresIn) * time.Second).Unix() + _ = config.SaveToken(t) + } + case t.KeyID != "" && t.KeySecret != "" && t.ServerURL != "": + // orkes access-key token: re-mint via POST /api/token. + if tok, exp, merr := auth.MintOrkesToken(t.ServerURL, t.KeyID, t.KeySecret); merr == nil { + t.AccessToken = tok + t.ExpiresAt = exp + _ = config.SaveToken(t) + } + } + } + return t.Header() +} + +// applyAuth attaches the auth header: X-Authorization (Auth0 JWT) when logged in, +// else a legacy Authorization: Bearer from a configured API key. orkes accepts both. +func (c *Client) applyAuth(req *http.Request) { + if c.authToken != "" { + req.Header.Set("X-Authorization", c.authToken) + } else if c.apiKey != "" { + req.Header.Set("Authorization", "Bearer "+c.apiKey) + } } func (c *Client) doRequest(method, path string, body interface{}) (*http.Response, error) { @@ -49,9 +97,7 @@ func (c *Client) doRequest(method, path string, body interface{}) (*http.Respons if body != nil { req.Header.Set("Content-Type", "application/json") } - if c.apiKey != "" { - req.Header.Set("Authorization", "Bearer "+c.apiKey) - } + c.applyAuth(req) resp, err := c.httpClient.Do(req) if err != nil { @@ -93,9 +139,7 @@ func (c *Client) doMultipartRequest(path string, manifest []byte, packageBytes [ return nil, err } req.Header.Set("Content-Type", writer.FormDataContentType()) - if c.apiKey != "" { - req.Header.Set("Authorization", "Bearer "+c.apiKey) - } + c.applyAuth(req) resp, err := c.httpClient.Do(req) if err != nil { @@ -196,9 +240,7 @@ func (c *Client) PollTask(taskType string) (map[string]interface{}, error) { if err != nil { return nil, err } - if c.apiKey != "" { - req.Header.Set("Authorization", "Bearer "+c.apiKey) - } + c.applyAuth(req) resp, err := c.httpClient.Do(req) if err != nil { @@ -578,9 +620,7 @@ func (c *Client) Stream(executionID string, lastEventID string, events chan<- SS if lastEventID != "" { req.Header.Set("Last-Event-ID", lastEventID) } - if c.apiKey != "" { - req.Header.Set("Authorization", "Bearer "+c.apiKey) - } + c.applyAuth(req) resp, err := streamClient.Do(req) if err != nil { @@ -645,34 +685,6 @@ func sseFieldValue(line, prefix string) string { // ─── Auth API ───────────────────────────────────────────────────────────────── -// LoginRequest is the payload for POST /api/auth/login -type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -// LoginResponse carries the JWT returned by the server -type LoginResponse struct { - Token string `json:"token"` -} - -// Login authenticates with the server and returns a JWT. -func (c *Client) Login(username, password string) (*LoginResponse, error) { - resp, err := c.doRequest("POST", "/api/auth/login", &LoginRequest{ - Username: username, - Password: password, - }) - if err != nil { - return nil, err - } - defer resp.Body.Close() - var result LoginResponse - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, fmt.Errorf("decode login response: %w", err) - } - return &result, nil -} - // ─── Credentials management API ─────────────────────────────────────────────────── // CredentialMeta is the list-view for a stored credential (from GET /api/secrets/v2). @@ -706,9 +718,7 @@ func (c *Client) SetCredential(name, value string) error { return err } req.Header.Set("Content-Type", "text/plain") - if c.apiKey != "" { - req.Header.Set("Authorization", "Bearer "+c.apiKey) - } + c.applyAuth(req) resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("request failed: %w", err) diff --git a/cli/cmd/credentials.go b/cli/cmd/credentials.go index 0f722c40d..6db14502f 100644 --- a/cli/cmd/credentials.go +++ b/cli/cmd/credentials.go @@ -6,7 +6,6 @@ import ( "text/tabwriter" "github.com/agentspan-ai/agentspan/cli/client" - "github.com/agentspan-ai/agentspan/cli/config" "github.com/fatih/color" "github.com/spf13/cobra" ) @@ -37,7 +36,7 @@ var secretsSetCmd = &cobra.Command{ } func runSecretsSet(name, value string) error { - cfg := config.Load() + cfg := getConfig() c := client.New(cfg) return c.SetCredential(name, value) } @@ -58,7 +57,7 @@ var secretsListCmd = &cobra.Command{ } func runSecretsList() (string, error) { - cfg := config.Load() + cfg := getConfig() c := client.New(cfg) secrets, err := c.ListCredentials() if err != nil { @@ -94,7 +93,7 @@ var secretsDeleteCmd = &cobra.Command{ } func runSecretsDelete(name string) error { - cfg := config.Load() + cfg := getConfig() return client.New(cfg).DeleteCredential(name) } diff --git a/cli/cmd/helpers.go b/cli/cmd/helpers.go index 7c31a7ff1..bc2a02fd7 100644 --- a/cli/cmd/helpers.go +++ b/cli/cmd/helpers.go @@ -4,6 +4,9 @@ package cmd import ( + "fmt" + "os" + "github.com/agentspan-ai/agentspan/cli/client" "github.com/agentspan-ai/agentspan/cli/config" ) @@ -12,6 +15,13 @@ func getConfig() *config.Config { cfg := config.Load() if serverURL != "" { cfg.ServerURL = serverURL + // An explicitly passed --server becomes the default for subsequent commands. + // Notice goes to stderr so piped stdout (JSON output etc.) stays clean. + if config.FileServerURL() != serverURL { + if err := config.SaveDefaultServer(serverURL); err == nil { + fmt.Fprintf(os.Stderr, "Default server set to %s (%s)\n", serverURL, config.ConfigDir()) + } + } } return cfg } diff --git a/cli/cmd/login.go b/cli/cmd/login.go index 3f25b9f5e..ad6796cc6 100644 --- a/cli/cmd/login.go +++ b/cli/cmd/login.go @@ -1,99 +1,308 @@ +// Copyright (c) 2025 AgentSpan +// Licensed under the MIT License. See LICENSE file in the project root for details. + package cmd import ( "bufio" "fmt" + "io" "os" + "os/exec" + "runtime" "strings" - "syscall" + "time" - "github.com/agentspan-ai/agentspan/cli/client" + "github.com/agentspan-ai/agentspan/cli/auth" "github.com/agentspan-ai/agentspan/cli/config" "github.com/fatih/color" "github.com/spf13/cobra" "golang.org/x/term" ) +var ( + loginDomain string + loginClientID string + loginScope string + loginNoOpen bool + loginKeyID string + loginKeySecret string + loginDevice bool + loginRedirectURI string +) + var loginCmd = &cobra.Command{ Use: "login", - Short: "Log in to the AgentSpan server and store an auth token", - Long: `Prompts for username and password, authenticates against the server, -and stores the returned JWT in ~/.agentspan/config.json. + Short: "Log in via your browser (Auth0) and store the token", + Long: `Logs in through your browser using the same Auth0 application the orkes-conductor +UI uses (Authorization Code + PKCE with a loopback redirect): your browser opens the +hosted login page (username/password, Google, SSO — whatever the tenant allows), the +redirect is captured locally, and the resulting token is stored in ~/.agentspan/token +and sent to the server as the X-Authorization header on every request. + +The Auth0 domain and client id are auto-discovered from the server's /context.js (the +same runtime config the UI consumes). Override with --auth0-domain / --auth0-client-id. + +Alternative grants: + --device OAuth Device Authorization flow (headless/SSH machines; + requires the Device Code grant enabled on the Auth0 app) + --key-id / --key-secret orkes application access key (service accounts / CI) -On localhost with auth disabled, this command is not required — the server -accepts all requests as anonymous admin automatically.`, +On a localhost server with auth disabled, login is not required — requests are accepted +as anonymous admin automatically.`, RunE: func(cmd *cobra.Command, args []string) error { cfg := getConfig() - if cfg.IsLocalhost() && cfg.APIKey == "" { - color.Yellow("Server is localhost — auth is optional.") - fmt.Println("Proceeding without login (anonymous admin mode).") - return nil + // No --server flag and no env override: ask which instance to log into + // (pre-filled with the current default; plain Enter keeps it). TTY only. + if serverURL == "" && + os.Getenv("AGENTSPAN_SERVER_URL") == "" && os.Getenv("AGENT_SERVER_URL") == "" && + term.IsTerminal(int(os.Stdin.Fd())) { + if entered := promptServerURL(os.Stdin, cfg.ServerURL); entered != "" { + cfg.ServerURL = entered + } } - fmt.Print("Username: ") - reader := bufio.NewReader(os.Stdin) - username, err := reader.ReadString('\n') - if err != nil { - return fmt.Errorf("read username: %w", err) + keyID := loginKeyID + if keyID == "" { + keyID = os.Getenv("AGENTSPAN_AUTH_KEY") } - username = strings.TrimSpace(username) - - fmt.Print("Password: ") - passwordBytes, err := term.ReadPassword(int(syscall.Stdin)) - fmt.Println() - if err != nil { - return fmt.Errorf("read password: %w", err) + keySecret := loginKeySecret + if keySecret == "" { + keySecret = os.Getenv("AGENTSPAN_AUTH_SECRET") } - password := string(passwordBytes) - if err := doLogin(cfg, username, password); err != nil { + var err error + if keyID != "" && keySecret != "" { + err = loginWithKey(cfg, keyID, keySecret) + } else { + err = loginWithAuth0(cfg) + } + if err != nil { return err } - color.Green("Logged in successfully.") - fmt.Printf("Token stored in %s/config.json\n", config.ConfigDir()) + // A successful login pins this server as the default for subsequent commands. + if config.FileServerURL() != cfg.ServerURL { + if serr := config.SaveDefaultServer(cfg.ServerURL); serr == nil { + color.Green("Default server set to %s", cfg.ServerURL) + } + } return nil }, } +// promptServerURL asks for the server URL, showing the current default. Returns the +// entered URL ("" = keep current). A missing scheme defaults to http:// (local instances). +func promptServerURL(r io.Reader, current string) string { + fmt.Printf("Server URL [%s]: ", current) + line, _ := bufio.NewReader(r).ReadString('\n') + s := strings.TrimSpace(line) + if s == "" { + return "" + } + if !strings.Contains(s, "://") { + s = "http://" + s + } + return strings.TrimRight(s, "/") +} + +// loginWithKey authenticates via an orkes application access key (keyId/keySecret) — +// the internal / service-account path: POST /api/token -> JWT. The key/secret are stored +// (0600) so the client can re-mint the token when it expires. +func loginWithKey(cfg *config.Config, keyID, keySecret string) error { + token, exp, err := auth.MintOrkesToken(cfg.ServerURL, keyID, keySecret) + if err != nil { + return err + } + if err := config.SaveToken(&config.TokenInfo{ + AccessToken: token, + ExpiresAt: exp, + KeyID: keyID, + KeySecret: keySecret, + ServerURL: cfg.ServerURL, + }); err != nil { + return fmt.Errorf("store token: %w", err) + } + color.Green("Logged in with access key. Token stored in %s", config.TokenPath()) + return nil +} + +// loginWithAuth0 runs the Auth0 device authorization grant (browser-based, matching the UI). +func loginWithAuth0(cfg *config.Config) error { + domain, clientID, useIDToken := loginDomain, loginClientID, false + if domain == "" || clientID == "" { + discovered, err := auth.DiscoverConfig(cfg.ServerURL) + if err != nil { + return fmt.Errorf( + "could not discover Auth0 config from %s (%w).\n"+ + "Pass --auth0-domain/--auth0-client-id, use --key-id/--key-secret, or confirm the server has auth enabled", + cfg.ServerURL, err) + } + if domain == "" { + domain = discovered.Domain + } + if clientID == "" { + clientID = discovered.ClientID + } + useIDToken = discovered.UseIDToken + } + + if loginDevice { + return loginWithDeviceFlow(domain, clientID, useIDToken) + } + // Default: browser Authorization Code + PKCE — the same grant the UI uses, so it + // works with the stock UI clientId (device flow requires a tenant-side grant toggle). + return loginWithBrowserPKCE(domain, clientID, useIDToken) +} + +// loginWithDeviceFlow runs the OAuth Device Authorization grant (for headless machines). +// Requires the Device Code grant to be enabled on the Auth0 application. +func loginWithDeviceFlow(domain, clientID string, useIDToken bool) error { + dc, err := auth.RequestDeviceCode(domain, clientID, loginScope) + if err != nil { + if strings.Contains(err.Error(), "unauthorized_client") { + return fmt.Errorf( + "the Device Authorization grant is not enabled on this Auth0 client.\n"+ + "Run without --device to use the browser login, or enable the Device Code grant "+ + "on the application in the Auth0 dashboard (%w)", err) + } + return err + } + + verifyURL := dc.VerificationURIComplete + if verifyURL == "" { + verifyURL = dc.VerificationURI + } + fmt.Println() + color.Cyan("To log in, open this URL in your browser:") + fmt.Printf(" %s\n", verifyURL) + color.Cyan("Confirm this code is shown: %s", dc.UserCode) + fmt.Println() + if !loginNoOpen { + _ = openBrowser(verifyURL) // best-effort; URL is printed regardless + } + fmt.Println("Waiting for you to complete login in the browser...") + + tok, err := auth.PollForToken(domain, clientID, dc) + if err != nil { + return err + } + return saveAuth0Token(tok, useIDToken, domain, clientID) +} + +// loginWithBrowserPKCE runs the Authorization Code + PKCE flow with a loopback redirect +// (RFC 8252): bind the redirect URI locally, open the hosted login in the browser, capture +// the ?code= from the redirect, and exchange it with the PKCE verifier. Works with the same +// public clientId the UI uses — the redirect URI must exactly match one of the Auth0 app's +// Allowed Callback URLs (the local UI origin, e.g. http://localhost:5001, is whitelisted). +func loginWithBrowserPKCE(domain, clientID string, useIDToken bool) error { + verifier, challenge, err := auth.GeneratePKCE() + if err != nil { + return err + } + state, err := auth.RandomState() + if err != nil { + return err + } + authorizeURL := auth.BuildAuthorizeURL(domain, clientID, loginRedirectURI, loginScope, state, challenge) + + // Bind the loopback port BEFORE printing anything or opening the browser — + // fail fast if it's busy (e.g. the UI dev server holds it). + capture, err := auth.StartCodeCapture(loginRedirectURI, state) + if err != nil { + return err + } + + fmt.Println() + color.Cyan("To log in, open this URL in your browser:") + fmt.Printf(" %s\n", authorizeURL) + fmt.Println() + if !loginNoOpen { + _ = openBrowser(authorizeURL) + } + fmt.Printf("Waiting for the browser login (redirect captured on %s)...\n", loginRedirectURI) + + code, err := capture.Wait(5 * time.Minute) + if err != nil { + return err + } + + tok, err := auth.ExchangeCode(domain, clientID, code, verifier, loginRedirectURI) + if err != nil { + return err + } + return saveAuth0Token(tok, useIDToken, domain, clientID) +} + +func saveAuth0Token(tok *auth.Token, useIDToken bool, domain, clientID string) error { + if err := config.SaveToken(&config.TokenInfo{ + AccessToken: tok.AccessToken, + IDToken: tok.IDToken, + RefreshToken: tok.RefreshToken, + ExpiresAt: time.Now().Add(time.Duration(tok.ExpiresIn) * time.Second).Unix(), + UseIDToken: useIDToken, + Auth0Domain: domain, + ClientID: clientID, + }); err != nil { + return fmt.Errorf("store token: %w", err) + } + color.Green("Logged in. Token stored in %s", config.TokenPath()) + return nil +} + var logoutCmd = &cobra.Command{ Use: "logout", Short: "Remove the stored auth token", RunE: func(cmd *cobra.Command, args []string) error { - cfg := config.Load() - if cfg.APIKey == "" { - color.Yellow("Not currently logged in.") - return nil + cleared := false + if t, _ := config.LoadToken(); t != nil { + if err := config.ClearToken(); err != nil { + return fmt.Errorf("clear token: %w", err) + } + cleared = true } - cfg.APIKey = "" - if err := config.Save(cfg); err != nil { - return fmt.Errorf("save config: %w", err) + // Also clear any legacy API key stored in config.json. + if c := config.Load(); c.APIKey != "" { + c.APIKey = "" + if err := config.Save(c); err != nil { + return fmt.Errorf("save config: %w", err) + } + cleared = true + } + if cleared { + color.Green("Logged out.") + } else { + color.Yellow("Not currently logged in.") } - color.Green("Logged out.") return nil }, } -// doLogin calls the server auth endpoint and persists the returned token. -// Extracted so tests can call it directly without terminal I/O. -func doLogin(cfg *config.Config, username, password string) error { - c := client.New(cfg) - resp, err := c.Login(username, password) - if err != nil { - return fmt.Errorf("login failed: %w", err) - } - if resp.Token == "" { - return fmt.Errorf("server returned empty token") +// openBrowser best-effort opens a URL in the default browser. +func openBrowser(url string) error { + var name string + var args []string + switch runtime.GOOS { + case "darwin": + name, args = "open", []string{url} + case "windows": + name, args = "rundll32", []string{"url.dll,FileProtocolHandler", url} + default: + name, args = "xdg-open", []string{url} } - cfg.APIKey = resp.Token - if err := config.Save(cfg); err != nil { - return fmt.Errorf("save config: %w", err) - } - return nil + return exec.Command(name, args...).Start() } func init() { + loginCmd.Flags().StringVar(&loginKeyID, "key-id", "", "orkes access key id (service-account auth; env AGENTSPAN_AUTH_KEY)") + loginCmd.Flags().StringVar(&loginKeySecret, "key-secret", "", "orkes access key secret (service-account auth; env AGENTSPAN_AUTH_SECRET)") + loginCmd.Flags().StringVar(&loginDomain, "auth0-domain", "", "Auth0 domain (default: discovered from server /context.js)") + loginCmd.Flags().StringVar(&loginClientID, "auth0-client-id", "", "Auth0 client id (default: discovered from server /context.js)") + loginCmd.Flags().StringVar(&loginScope, "scope", auth.DefaultScope, "OAuth scope to request") + loginCmd.Flags().BoolVar(&loginNoOpen, "no-open", false, "Do not auto-open the browser") + loginCmd.Flags().BoolVar(&loginDevice, "device", false, "Use the OAuth Device Authorization flow instead of the browser login (headless machines)") + loginCmd.Flags().StringVar(&loginRedirectURI, "redirect-uri", "http://localhost:5001", "Loopback redirect URI for the browser login; must exactly match an Allowed Callback URL of the Auth0 app (default: the local orkes UI origin)") rootCmd.AddCommand(loginCmd) rootCmd.AddCommand(logoutCmd) } diff --git a/cli/cmd/login_test.go b/cli/cmd/login_test.go index 61d6d8e19..69ee03bfa 100644 --- a/cli/cmd/login_test.go +++ b/cli/cmd/login_test.go @@ -1,107 +1,104 @@ package cmd import ( - "encoding/json" - "net/http" - "net/http/httptest" + "strings" "testing" + "time" "github.com/agentspan-ai/agentspan/cli/config" ) -func TestLogoutClearsAPIKey(t *testing.T) { - newTempHome(t) - - cfg := config.DefaultConfig() - cfg.APIKey = "existing-token" - if err := config.Save(cfg); err != nil { - t.Fatalf("save: %v", err) +func TestPromptServerURL(t *testing.T) { + // Enter accepts the current default. + if got := promptServerURL(strings.NewReader("\n"), "http://localhost:6767"); got != "" { + t.Errorf("enter should keep current, got %q", got) } - - cfg.APIKey = "" - if err := config.Save(cfg); err != nil { - t.Fatalf("save cleared: %v", err) + // Full URL passes through (trailing slash trimmed). + if got := promptServerURL(strings.NewReader("https://my.orkes.io/\n"), "x"); got != "https://my.orkes.io" { + t.Errorf("got %q", got) } - - loaded := config.Load() - if loaded.APIKey != "" { - t.Errorf("APIKey after logout = %q, want empty", loaded.APIKey) + // Missing scheme defaults to http:// (local instances). + if got := promptServerURL(strings.NewReader("localhost:8080\n"), "x"); got != "http://localhost:8080" { + t.Errorf("got %q", got) } } -func TestLoginStoresToken(t *testing.T) { +func TestGetConfigPersistsExplicitServer(t *testing.T) { newTempHome(t) + old := serverURL + defer func() { serverURL = old }() - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost || r.URL.Path != "/api/auth/login" { - http.NotFound(w, r) - return - } - var body map[string]string - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - http.Error(w, "bad body", http.StatusBadRequest) - return - } - if body["username"] != "alice" || body["password"] != "secret" { - http.Error(w, "unauthorized", http.StatusUnauthorized) - return - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"token": "jwt-abc123"}) - })) - defer srv.Close() + serverURL = "http://localhost:8080" + cfg := getConfig() + if cfg.ServerURL != "http://localhost:8080" { + t.Fatalf("effective server = %q", cfg.ServerURL) + } + // The explicit --server must now be the persisted default. + if got := config.FileServerURL(); got != "http://localhost:8080" { + t.Errorf("persisted default = %q, want http://localhost:8080", got) + } + // And a flag-less invocation picks it up. + serverURL = "" + if cfg2 := getConfig(); cfg2.ServerURL != "http://localhost:8080" { + t.Errorf("flag-less server = %q, want persisted default", cfg2.ServerURL) + } +} - cfg := config.DefaultConfig() - cfg.ServerURL = srv.URL - if err := config.Save(cfg); err != nil { +func TestSaveDefaultServerPreservesAPIKey(t *testing.T) { + newTempHome(t) + if err := config.Save(&config.Config{ServerURL: "http://a", APIKey: "keep-me"}); err != nil { t.Fatalf("save: %v", err) } - - if err := doLogin(cfg, "alice", "secret"); err != nil { - t.Fatalf("doLogin: %v", err) + if err := config.SaveDefaultServer("http://b"); err != nil { + t.Fatalf("SaveDefaultServer: %v", err) } - - loaded := config.Load() - if loaded.APIKey != "jwt-abc123" { - t.Errorf("APIKey = %q, want jwt-abc123", loaded.APIKey) + cfg := config.Load() + if cfg.ServerURL != "http://b" || cfg.APIKey != "keep-me" { + t.Errorf("got %+v, want server http://b with api key preserved", cfg) } } -func TestLoginServerError(t *testing.T) { +func TestLogoutClearsToken(t *testing.T) { newTempHome(t) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "unauthorized", http.StatusUnauthorized) - })) - defer srv.Close() + if err := config.SaveToken(&config.TokenInfo{ + AccessToken: "jwt-abc", + ExpiresAt: time.Now().Add(time.Hour).Unix(), + }); err != nil { + t.Fatalf("save token: %v", err) + } - cfg := config.DefaultConfig() - cfg.ServerURL = srv.URL - if err := config.Save(cfg); err != nil { - t.Fatalf("save: %v", err) + if err := config.ClearToken(); err != nil { + t.Fatalf("clear token: %v", err) } - if err := doLogin(cfg, "bad", "creds"); err == nil { - t.Fatal("expected error from doLogin on 401, got nil") + tok, err := config.LoadToken() + if err != nil { + t.Fatalf("load token: %v", err) + } + if tok != nil { + t.Errorf("token after logout = %+v, want nil", tok) } } -func TestLoginEmptyTokenError(t *testing.T) { - newTempHome(t) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"token": ""}) - })) - defer srv.Close() - - cfg := config.DefaultConfig() - cfg.ServerURL = srv.URL - if err := config.Save(cfg); err != nil { - t.Fatalf("save: %v", err) +func TestTokenHeaderSelection(t *testing.T) { + access := &config.TokenInfo{AccessToken: "acc", IDToken: "id", UseIDToken: false} + if got := access.Header(); got != "acc" { + t.Errorf("default header = %q, want access token", got) + } + idtok := &config.TokenInfo{AccessToken: "acc", IDToken: "id", UseIDToken: true} + if got := idtok.Header(); got != "id" { + t.Errorf("useIdToken header = %q, want id token", got) } +} - if err := doLogin(cfg, "user", "pass"); err == nil { - t.Fatal("expected error for empty token, got nil") +func TestTokenExpired(t *testing.T) { + expired := &config.TokenInfo{ExpiresAt: time.Now().Add(-time.Minute).Unix()} + if !expired.Expired() { + t.Error("expected expired token to report Expired()=true") + } + fresh := &config.TokenInfo{ExpiresAt: time.Now().Add(time.Hour).Unix()} + if fresh.Expired() { + t.Error("expected fresh token to report Expired()=false") } } diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 9c1b7a58e..d61283e4e 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -58,7 +58,7 @@ func Execute() { } func init() { - rootCmd.PersistentFlags().StringVar(&serverURL, "server", "", "Runtime server URL (default: http://localhost:6767)") + rootCmd.PersistentFlags().StringVar(&serverURL, "server", "", "Runtime server URL; once passed it is saved as the default for subsequent commands (initial default: http://localhost:6767)") rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(tuiCmd) } diff --git a/cli/config/config.go b/cli/config/config.go index ceb327e1e..96ccfa843 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -83,3 +83,29 @@ func Save(cfg *Config) error { } return os.WriteFile(configPath(), data, 0o600) } + +// FileServerURL returns the server URL stored in config.json (no env/default merging). +// Empty when no config file exists or it has no server_url. +func FileServerURL() string { + data, err := os.ReadFile(configPath()) + if err != nil { + return "" + } + var fileCfg Config + if json.Unmarshal(data, &fileCfg) != nil { + return "" + } + return fileCfg.ServerURL +} + +// SaveDefaultServer persists serverURL as the default in config.json, preserving any +// other stored fields (e.g. a legacy api_key). Used so an explicitly passed --server +// (or the URL confirmed at login) becomes the default for subsequent commands. +func SaveDefaultServer(serverURL string) error { + fileCfg := &Config{} + if data, err := os.ReadFile(configPath()); err == nil { + _ = json.Unmarshal(data, fileCfg) + } + fileCfg.ServerURL = serverURL + return Save(fileCfg) +} diff --git a/cli/config/token.go b/cli/config/token.go new file mode 100644 index 000000000..6640cdbc3 --- /dev/null +++ b/cli/config/token.go @@ -0,0 +1,85 @@ +// Copyright (c) 2025 AgentSpan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package config + +import ( + "encoding/json" + "os" + "path/filepath" + "time" +) + +// TokenInfo is the auth token persisted at ~/.agentspan/token. It holds the Auth0 +// JWT sent to orkes as the X-Authorization header, plus the refresh token and the +// issuer details needed to refresh it without re-login. +type TokenInfo struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + ExpiresAt int64 `json:"expires_at"` // unix seconds + UseIDToken bool `json:"use_id_token"` + Auth0Domain string `json:"auth0_domain,omitempty"` + ClientID string `json:"client_id,omitempty"` + + // orkes internal-key (service-account) grant: re-mint via POST {ServerURL}/api/token. + KeyID string `json:"key_id,omitempty"` + KeySecret string `json:"key_secret,omitempty"` + ServerURL string `json:"server_url,omitempty"` +} + +// TokenPath is the dedicated token file: ~/.agentspan/token. +func TokenPath() string { + return filepath.Join(ConfigDir(), "token") +} + +// Header returns the JWT to send as X-Authorization: the ID token when the +// deployment is configured for it, otherwise the access token (UI default). +func (t *TokenInfo) Header() string { + if t.UseIDToken && t.IDToken != "" { + return t.IDToken + } + return t.AccessToken +} + +// Expired reports whether the token is at/near expiry (30s clock-skew margin). +func (t *TokenInfo) Expired() bool { + return t.ExpiresAt > 0 && time.Now().Unix() >= t.ExpiresAt-30 +} + +// SaveToken writes the token file with 0600 perms. +func SaveToken(t *TokenInfo) error { + if err := os.MkdirAll(ConfigDir(), 0o700); err != nil { + return err + } + data, err := json.MarshalIndent(t, "", " ") + if err != nil { + return err + } + return os.WriteFile(TokenPath(), data, 0o600) +} + +// LoadToken reads the token file. Returns (nil, nil) when no token is stored. +func LoadToken() (*TokenInfo, error) { + data, err := os.ReadFile(TokenPath()) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + var t TokenInfo + if err := json.Unmarshal(data, &t); err != nil { + return nil, err + } + return &t, nil +} + +// ClearToken removes the token file (logout). No-op if absent. +func ClearToken() error { + err := os.Remove(TokenPath()) + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} diff --git a/docs/design/plans/2026-03-23-multi-language-sdk-deliverables.md b/docs/design/plans/2026-03-23-multi-language-sdk-deliverables.md index 1b62a69eb..f258bfadb 100644 --- a/docs/design/plans/2026-03-23-multi-language-sdk-deliverables.md +++ b/docs/design/plans/2026-03-23-multi-language-sdk-deliverables.md @@ -52,7 +52,7 @@ Key requirements for each stage (referencing spec traceability matrix feature nu - **Stage 2** (features 4, 10, 11, 12, 18, 19, 21, 52, 53, 55, 56, 76): Parallel + scatter_gather + all tool types + credentials + ToolContext + external tool - **Stage 3** (features 3, 31, 32, 39, 62, 77): Sequential (>>) + memory + callbacks (all 6 positions) + stop_when - **Stage 4** (features 20, 22-29): All guardrail types + all OnFail modes + tool guardrails -- **Stage 5** (features 17, 40-42, 65): HITL approve + reject + feedback + UserProxyAgent + human_tool +- **Stage 5** (features 17, 40-42): HITL approve + reject + feedback + human_tool - **Stage 6** (features 6-9, 35, 37, 38): All remaining strategies + OnTextMention + introductions + transitions - **Stage 7** (features 2, 33, 34, 36, 71, 88): Handoff + termination + handoff conditions + gate + external agent - **Stage 8** (features 13, 15, 16, 58-61, 64, 66-70, 72): Code execution + media + RAG + agent_tool + GPTAssistant + thinking + include_contents + required_tools + planner + CLI config @@ -264,7 +264,7 @@ from agentspan.agents import ( CodeExecutionConfig, CodeExecutor, LocalCodeExecutor, DockerCodeExecutor, JupyterCodeExecutor, ServerlessCodeExecutor, ExecutionResult, # Extended - UserProxyAgent, GPTAssistantAgent, CallbackHandler, CliConfig, + GPTAssistantAgent, CallbackHandler, CliConfig, # Credentials get_credential, CredentialFile, # Execution (top-level convenience + runtime) @@ -554,7 +554,7 @@ review_agent = Agent( # ═══════════════════════════════════════════════════════════════════════ # STAGE 5: Editorial Approval # Features: #17 approval_required, #40 approve, #41 reject, -# #42 feedback/respond, #14 human_tool, #65 UserProxyAgent +# #42 feedback/respond, #14 human_tool # ═══════════════════════════════════════════════════════════════════════ @tool(approval_required=True) @@ -572,19 +572,11 @@ editorial_question = human_tool( }, ) -editorial_reviewer = UserProxyAgent( - name="editorial_reviewer", - model=settings.llm_model, - instructions="You are the editorial reviewer. Provide feedback on article quality.", - human_input_mode="TERMINATE", -) - editorial_agent = Agent( name="editorial_approval", model=settings.llm_model, instructions="Review the article, ask questions, get approval before publishing.", tools=[publish_article, editorial_question], - agents=[editorial_reviewer], strategy=Strategy.HANDOFF, ) diff --git a/docs/design/plans/2026-03-24-typescript-sdk-implementation.md b/docs/design/plans/2026-03-24-typescript-sdk-implementation.md index 7f0bf611e..b61680907 100644 --- a/docs/design/plans/2026-03-24-typescript-sdk-implementation.md +++ b/docs/design/plans/2026-03-24-typescript-sdk-implementation.md @@ -35,7 +35,7 @@ | `sdk/typescript/src/handoff.ts` | OnToolResult, OnTextMention, OnCondition | | `sdk/typescript/src/callback.ts` | CallbackHandler base class (6 positions) | | `sdk/typescript/src/code-execution.ts` | CodeExecutor abstract, Local/Docker/Jupyter/Serverless, asTool() | -| `sdk/typescript/src/ext.ts` | UserProxyAgent, GPTAssistantAgent | +| `sdk/typescript/src/ext.ts` | GPTAssistantAgent | | `sdk/typescript/src/discovery.ts` | discoverAgents(path) | | `sdk/typescript/src/tracing.ts` | OpenTelemetry integration | | `sdk/typescript/src/frameworks/detect.ts` | detectFramework() duck-typing | @@ -449,7 +449,7 @@ Test: individual conditions toJSON, composition (and/or nesting), TextGate toJSO - [ ] **Step 3: Write ext.ts** -`UserProxyAgent` extends Agent (modes: ALWAYS/TERMINATE/NEVER). `GPTAssistantAgent` extends Agent (assistantId, thread support). +`GPTAssistantAgent` extends Agent (assistantId, thread support). - [ ] **Step 4: Write discovery.ts** diff --git a/docs/design/specs/2026-03-23-typescript-sdk-design.md b/docs/design/specs/2026-03-23-typescript-sdk-design.md index 713adcf09..29ec98856 100644 --- a/docs/design/specs/2026-03-23-typescript-sdk-design.md +++ b/docs/design/specs/2026-03-23-typescript-sdk-design.md @@ -177,7 +177,7 @@ sdk/typescript/ types.ts # Shared interfaces, enums, ToolContext, EventType, Status serializer.ts # Agent → AgentConfig JSON (recursive, handles all tool types) config.ts # AgentConfig env var loading, URL normalization - ext.ts # UserProxyAgent, GPTAssistantAgent + ext.ts # GPTAssistantAgent discovery.ts # discoverAgents(path) tracing.ts # OpenTelemetry integration frameworks/ @@ -1463,24 +1463,6 @@ All passthrough normalizers produce the same structure: ## 15. Extended Types -### 15.1 UserProxyAgent - -Human stand-in for multi-agent conversations. - -```typescript -export class UserProxyAgent extends Agent { - constructor(options: { - name: string; - mode: 'ALWAYS' | 'TERMINATE' | 'NEVER'; - instructions?: string; - }); -} -``` - -- `ALWAYS` — always pause for human input -- `TERMINATE` — pause only on termination condition -- `NEVER` — auto-respond (useful for testing) - ### 15.2 GPTAssistantAgent Wraps OpenAI Assistants API. @@ -1790,7 +1772,7 @@ Per base spec §12, with TypeScript-specific additions: 13. **Termination + Handoffs** — `termination.ts`, `handoff.ts`, composable conditions 14. **Callbacks** — `callback.ts`, 6-position lifecycle hooks 15. **Code execution** — `code-execution.ts`, all 4 executors + asTool() -16. **Extended types** — `ext.ts`, UserProxyAgent + GPTAssistantAgent +16. **Extended types** — `ext.ts`, GPTAssistantAgent 17. **Framework integration** — `frameworks/`, detection + 5 worker factories + event push 18. **Testing framework** — `testing/`, mockRun + expect + assertions + record/replay + eval 19. **Validation framework** — `validation/`, runner + judge + report diff --git a/docs/java-sdk b/docs/java-sdk new file mode 120000 index 000000000..49439def6 --- /dev/null +++ b/docs/java-sdk @@ -0,0 +1 @@ +../sdk/java/docs \ No newline at end of file diff --git a/docs/python-sdk/agent-configuration.md b/docs/python-sdk/agent-configuration.md index 9988c0c33..a983c6321 100644 --- a/docs/python-sdk/agent-configuration.md +++ b/docs/python-sdk/agent-configuration.md @@ -856,33 +856,6 @@ for event in runtime.stream(agent, "Hello"): ## Extended Agent Types -### UserProxyAgent - -A human stand-in agent. When it's this agent's turn, the execution pauses with a `HumanTask` and waits for real human input. Useful in multi-agent conversations where a human participates. - -```python -from agentspan.agents import UserProxyAgent, Agent - -user = UserProxyAgent(name="human") -assistant = Agent(name="assistant", model="openai/gpt-4o") - -team = Agent( - name="chat", - model="openai/gpt-4o", - agents=[user, assistant], - strategy="round_robin", - max_turns=6, -) -``` - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `name` | `str` | `"user"` | Agent name. | -| `human_input_mode` | `str` | `"ALWAYS"` | When to request human input: `"ALWAYS"`, `"TERMINATE"`, or `"NEVER"`. | -| `default_response` | `str` | `"Continue."` | Response when `human_input_mode="NEVER"` (pass-through). | -| `model` | `str` | `"openai/gpt-4o"` | LLM model (only used when generating a fallback response). | -| `instructions` | `str \| Callable` | auto | System prompt. Defaults to relaying human input exactly. | - ### GPTAssistantAgent Wraps an OpenAI Assistant (with its own instructions, tools, and file search) as a Conductor Agent. diff --git a/docs/python-sdk/api-reference.md b/docs/python-sdk/api-reference.md index 69626244b..cd972b3cd 100644 --- a/docs/python-sdk/api-reference.md +++ b/docs/python-sdk/api-reference.md @@ -1181,7 +1181,6 @@ All runnable examples must execute successfully against a live Conductor server | `09c_hitl_streaming.py` | Requires human interaction | | `18_manual_selection.py` | Requires human interaction | | `26_opentelemetry_tracing.py` | Requires OTel collector | -| `27_user_proxy_agent.py` | Requires human interaction | | `32_human_guardrail.py` | Requires human review | ### Run All Autonomous Examples diff --git a/docs/python-sdk/compilation-comparison.md b/docs/python-sdk/compilation-comparison.md index b7b507daf..437da64aa 100644 --- a/docs/python-sdk/compilation-comparison.md +++ b/docs/python-sdk/compilation-comparison.md @@ -36,7 +36,6 @@ Server: http://localhost:6767/api | 22_llm_guardrails | agent | MATCH | | | 23_token_tracking | agent | MATCH | | | 25_semantic_memory | agent | MATCH | | -| 27_user_proxy_agent | conversation | MATCH | | | 29_agent_introductions | design_review | MATCH | | | 30_multimodal_agent | creative_pipeline | MATCH | | | 31_tool_guardrails | agent | MATCH | | diff --git a/docs/sdk-design/2026-03-23-multi-language-sdk-design.md b/docs/sdk-design/2026-03-23-multi-language-sdk-design.md index 291688883..10949562c 100644 --- a/docs/sdk-design/2026-03-23-multi-language-sdk-design.md +++ b/docs/sdk-design/2026-03-23-multi-language-sdk-design.md @@ -1097,12 +1097,6 @@ Callbacks are registered as Conductor workers (same as tools). The server dispat ### 4.13 Extended Agent Types -#### UserProxyAgent - -Human stand-in agent for multi-agent conversations: -- Modes: `ALWAYS` (always ask), `TERMINATE` (ask on termination), `NEVER` (auto-respond) -- Wraps HITL interaction as an agent in the team - #### GPTAssistantAgent Wraps OpenAI Assistants API: @@ -1582,12 +1576,11 @@ A single mega-workflow that processes an article request through a complete publ - Tool guardrail: validates tool inputs before execution #### Stage 5 — Editorial Approval -**Features:** HITL (all modes), UserProxyAgent, human_tool, streaming + HITL +**Features:** HITL (all modes), human_tool, streaming + HITL - `approval_required=True` on publish tool → durable pause - `human_tool()` for inline editorial questions - `handle.respond()` with feedback for revision loop -- UserProxyAgent participates as editorial reviewer - Streaming events show real-time progress + pause notification #### Stage 6 — Translation & Discussion @@ -1860,7 +1853,6 @@ Every feature must be traceable from concept → Python reference → wire forma | 62 | Callbacks | `callback.py:CallbackHandler` | `agentConfig.callbacks` | Worker dispatch | Stage 3 | | 63 | PromptTemplate | `agent.py:PromptTemplate` | `instructions.type="prompt_template"` | Server-side lookup | Stage 1 | | 64 | Token tracking | `result.py:TokenUsage` | Status response | LLM_CHAT_COMPLETE | All | -| 65 | UserProxyAgent | `ext.py:UserProxyAgent` | AgentConfig + HUMAN | Hybrid | Stage 5 | | 66 | GPTAssistantAgent | `ext.py:GPTAssistantAgent` | AgentConfig + threads | Hybrid | Stage 8 | | 67 | Extended thinking | `agent.py:thinking_budget_tokens` | `agentConfig.thinkingConfig` | LLM param | Cross-cutting | | 68 | Include contents | `agent.py:include_contents` | `agentConfig.includeContents` | Prompt injection | Cross-cutting | @@ -1904,7 +1896,7 @@ Recommended order for implementing a new SDK: 10. **Memory** — ConversationMemory, SemanticMemory 11. **Termination + Handoffs** — composable conditions 12. **Code execution** — all executor types -13. **Extended types** — UserProxyAgent, GPTAssistantAgent +13. **Extended types** — GPTAssistantAgent 14. **Callbacks** — lifecycle hooks 15. **Framework integration** — detection, extraction, compilation to AgentConfig (TypeScript: Vercel AI SDK; Python: LangGraph, LangChain) 16. **Testing framework** — mock, expect, assertions, record/replay @@ -1965,7 +1957,6 @@ These cover every feature of the native SDK. Each new SDK must implement all of | 24 | `code_execution` | CodeExecutionConfig | | 25 | `semantic_memory` | SemanticMemory + MemoryStore | | 26 | `opentelemetry_tracing` | OTel integration | -| 27 | `user_proxy_agent` | UserProxyAgent | | 28 | `gpt_assistant_agent` | GPTAssistantAgent | | 29 | `agent_introductions` | introduction field | | 30 | `multimodal_agent` | Media tools (image, audio, video, pdf) | diff --git a/docs/sdk-design/go.md b/docs/sdk-design/go.md index b53c9f7af..f3d6014f3 100644 --- a/docs/sdk-design/go.md +++ b/docs/sdk-design/go.md @@ -39,7 +39,7 @@ sdk/go/ run.go # Top-level Run, Start, Stream, Deploy, etc. serialize.go # AgentConfig JSON serialization errors.go # Error types - ext.go # UserProxyAgent, GPTAssistantAgent + ext.go # GPTAssistantAgent discovery.go # DiscoverAgents tracing.go # IsTracingEnabled agentspan/sse/ # internal SSE client @@ -1655,16 +1655,9 @@ editorialQuestion := agentspan.HumanTool("ask_editor", "Ask the editor a question about the article.", ) -editorialReviewer := agentspan.NewUserProxyAgent("editorial_reviewer", - agentspan.WithModel(llmModel), - agentspan.WithInstructions("You are the editorial reviewer."), - agentspan.WithHumanInputMode("TERMINATE"), -) - editorialAgent := agentspan.NewAgent("editorial_approval", agentspan.WithModel(llmModel), agentspan.WithTools(publishTool, editorialQuestion), - agentspan.WithSubAgents(editorialReviewer), agentspan.WithStrategy(agentspan.StrategyHandoff), ) ``` diff --git a/docs/sdk-design/java.md b/docs/sdk-design/java.md index 3df861603..73d5cf4ef 100644 --- a/docs/sdk-design/java.md +++ b/docs/sdk-design/java.md @@ -140,7 +140,7 @@ src/main/java/dev/agentspan/ code/ // CodeExecutionConfig, CodeExecutor (Local/Docker/Jupyter/Serverless) credential/ // Credentials, CredentialFile callback/ // CallbackHandler - ext/ // UserProxyAgent, GPTAssistantAgent + ext/ // GPTAssistantAgent gate/ // GateCondition, TextGate cli/ // CliConfig exception/ // AgentspanException hierarchy diff --git a/docs/sdk-design/kitchen-sink.md b/docs/sdk-design/kitchen-sink.md index d9fb62472..66b4b5b19 100644 --- a/docs/sdk-design/kitchen-sink.md +++ b/docs/sdk-design/kitchen-sink.md @@ -150,7 +150,6 @@ The workflow processes this through 9 stages, each targeting a specific feature - `#41` `handle.reject(reason)` / `stream.reject(reason)` - `#42` `handle.send(message)` / `stream.send(message)` (feedback) - `#14` `human_tool()` for inline editorial questions -- `#65` `UserProxyAgent` as editorial reviewer **Expected behavior:** - `publish_article` tool has `approval_required=True` → workflow pauses with `WAITING` event @@ -158,7 +157,6 @@ The workflow processes this through 9 stages, each targeting a specific feature - Second HITL interaction: `stream.reject("Title needs improvement")` — rejection - Third HITL interaction: `stream.approve()` — approval - `human_tool("ask_editor")` allows agent to ask editor questions during review -- `UserProxyAgent("editorial_reviewer")` participates in review discussion **Assertions:** - `WAITING` event emitted at least once diff --git a/docs/sdk-design/kotlin.md b/docs/sdk-design/kotlin.md index b236d5054..f76a87a24 100644 --- a/docs/sdk-design/kotlin.md +++ b/docs/sdk-design/kotlin.md @@ -979,7 +979,7 @@ val reviewAgent = agent("safety_reviewer") { // ═══════════════════════════════════════════════════════════════ // STAGE 5: Editorial Approval -// Features: HITL, UserProxyAgent, human_tool +// Features: HITL, human_tool // ═══════════════════════════════════════════════════════════════ val publishArticle = tool("publish_article") { @@ -997,19 +997,11 @@ val editorialQuestion = humanTool( }, ) -val editorialReviewer = UserProxyAgent( - name = "editorial_reviewer", - model = llmModel, - instructions = "You are the editorial reviewer. Provide feedback on article quality.", - humanInputMode = HumanInputMode.TERMINATE, -) - val editorialAgent = agent("editorial_approval") { model(llmModel) instructions("Review the article, ask questions, get approval before publishing.") tool(publishArticle) tool(editorialQuestion) - agent(editorialReviewer) strategy = Strategy.HANDOFF } diff --git a/docs/sdk-design/ruby.md b/docs/sdk-design/ruby.md index 1b8c17623..651caf73e 100644 --- a/docs/sdk-design/ruby.md +++ b/docs/sdk-design/ruby.md @@ -42,7 +42,6 @@ agentspan-ruby/ http_client.rb # REST wrapper (Faraday or Net::HTTP) errors.rb # Exception hierarchy ext/ - user_proxy_agent.rb gpt_assistant_agent.rb testing/ mock.rb # mock_run @@ -1865,18 +1864,10 @@ editorial_question = Agentspan.human_tool( } ) -editorial_reviewer = Agentspan::UserProxyAgent.new( - name: "editorial_reviewer", - model: LLM_MODEL, - instructions: "You are the editorial reviewer. Provide feedback on article quality.", - human_input_mode: "TERMINATE" -) - editorial_agent = Agentspan::Agent.new("editorial_approval", model: LLM_MODEL, instructions: "Review the article, ask questions, get approval before publishing.", tools: [publish_article, editorial_question], - agents: [editorial_reviewer], strategy: Agentspan::Strategy::HANDOFF ) ``` diff --git a/docs/sdk-design/typescript.md b/docs/sdk-design/typescript.md index 44984ccce..c82a0aebe 100644 --- a/docs/sdk-design/typescript.md +++ b/docs/sdk-design/typescript.md @@ -1619,10 +1619,10 @@ const reviewAgent = new Agent({ ### Stage 5: Editorial Approval -**Python features:** `approval_required`, `human_tool`, `UserProxyAgent`, `Strategy.HANDOFF`, HITL interactions +**Python features:** `approval_required`, `human_tool`, `Strategy.HANDOFF`, HITL interactions ```typescript -import { tool, humanTool, UserProxyAgent, Agent } from "agentspan"; +import { tool, humanTool, Agent } from "agentspan"; const publishArticle = tool( async (args: { title: string; content: string; platform: string }) => ({ @@ -1637,18 +1637,10 @@ const editorialQuestion = humanTool({ inputSchema: { type: "object", properties: { question: { type: "string" } }, required: ["question"] }, }); -const editorialReviewer = new UserProxyAgent({ - name: "editorial_reviewer", - model: LLM_MODEL, - instructions: "You are the editorial reviewer.", - humanInputMode: "TERMINATE", -}); - const editorialAgent = new Agent({ name: "editorial_approval", model: LLM_MODEL, tools: [publishArticle, editorialQuestion], - agents: [editorialReviewer], strategy: "handoff", }); ``` diff --git a/mkdocs.yml b/mkdocs.yml index 23c83fed0..3d520f268 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,6 +80,28 @@ nav: - LangGraph Code Review Bot: examples/langgraph.md - OpenAI Agents SDK Customer Support: examples/openai-agents-sdk.md - Google ADK Research Assistant: examples/google-adk.md + - Java SDK: + - Overview: java-sdk/index.md + - Getting Started: java-sdk/getting-started.md + - Core Concepts: + - Agents: java-sdk/concepts/agents.md + - Agent Field Reference: java-sdk/agent-structure.md + - Agent JSON Schema: java-sdk/agent-schema.md + - Tools: java-sdk/concepts/tools.md + - Multi-Agent: java-sdk/concepts/multi-agent.md + - Guardrails: java-sdk/concepts/guardrails.md + - Termination: java-sdk/concepts/termination.md + - Scheduling: java-sdk/concepts/scheduling.md + - Skills: java-sdk/concepts/skills.md + - Frameworks: + - LangChain4j: java-sdk/frameworks/langchain4j.md + - OpenAI Agents SDK: java-sdk/frameworks/openai.md + - Google ADK: java-sdk/frameworks/google-adk.md + - Spring Boot: java-sdk/spring-boot.md + - API Reference: + - AgentRuntime: java-sdk/agent-runtime-api.md + - AgentClient (internal): java-sdk/agent-client-api.md + - Public API summary: java-sdk/api-reference.md - Reference: - Providers: providers.md - AI Models: ai-models.md diff --git a/sdk/csharp/CHANGELOG.md b/sdk/csharp/CHANGELOG.md index 019d9edc7..1723c0ac0 100644 --- a/sdk/csharp/CHANGELOG.md +++ b/sdk/csharp/CHANGELOG.md @@ -32,7 +32,7 @@ First public release of the Agentspan .NET SDK. - **Thinking config** — extended thinking budget for supported models - **Shared state** — typed shared state across agents in a workflow - **Planner** — agent planning mode -- **UserProxyAgent** / **GPTAssistantAgent** — compatibility agents +- **GPTAssistantAgent** — compatibility agents - **`DeployAsync` / `ServeAsync` / `RunByNameAsync`** — deploy, serve, and trigger named agents #### Examples (93 total) diff --git a/sdk/csharp/examples/27_UserProxyAgent/Example27UserProxyAgent.csproj b/sdk/csharp/examples/27_UserProxyAgent/Example27UserProxyAgent.csproj deleted file mode 100644 index aa44b46c2..000000000 --- a/sdk/csharp/examples/27_UserProxyAgent/Example27UserProxyAgent.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - Exe - net10.0 - enable - enable - - - - - - diff --git a/sdk/csharp/examples/27_UserProxyAgent/Program.cs b/sdk/csharp/examples/27_UserProxyAgent/Program.cs deleted file mode 100644 index 715015fd0..000000000 --- a/sdk/csharp/examples/27_UserProxyAgent/Program.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. - -// UserProxyAgent — human stand-in for interactive conversations. -// -// UserProxyAgent.Create() builds an Agent with metadata that tells the -// server to pause when it's the proxy's turn and await human input via -// AgentHandle.RespondAsync. -// -// Modes: -// Always — always pause for human input (this example) -// Terminate — pause only when the conversation would end -// Never — auto-respond, useful for testing -// -// Requirements: -// - Agentspan server with LLM support -// - AGENTSPAN_SERVER_URL=http://localhost:6767/api in environment -// - AGENTSPAN_LLM_MODEL set in environment - -using Agentspan; -using Agentspan.Examples; - -// ── Human proxy ─────────────────────────────────────────────────────── - -var human = UserProxyAgent.Create(name: "human", humanInputMode: HumanInputMode.Always); - -// ── AI coding assistant ─────────────────────────────────────────────── - -var assistant = new Agent("assistant") -{ - Model = Settings.LlmModel, - Instructions = - "You are a helpful coding assistant. Help the user write Python code. " + - "Ask clarifying questions when needed.", -}; - -// ── Round-robin: human and assistant take turns (2 exchanges) ───────── - -var conversation = new Agent("pair_programming") -{ - Model = Settings.LlmModel, - Agents = [human, assistant], - Strategy = Strategy.RoundRobin, - MaxTurns = 4, // human, assistant, human, assistant -}; - -// ── Run with streaming — respond when the proxy waits ──────────────── - -await using var runtime = new AgentRuntime(); -var handle = await runtime.StartAsync( - conversation, - "Let's write a Python function to sort a list of dictionaries by a key."); - -Console.WriteLine($"Started: {handle.ExecutionId}\n"); - -await foreach (var ev in handle.StreamAsync()) -{ - switch (ev.Type) - { - case EventType.Thinking: - Console.WriteLine($" [thinking] {ev.Content}"); - break; - - case EventType.ToolCall: - Console.WriteLine($" [tool_call] {ev.ToolName}({ev.Args})"); - break; - - case EventType.ToolResult: - Console.WriteLine($" [tool_result] {ev.ToolName} -> {ev.Result}"); - break; - - case EventType.Waiting: - Console.Write("\n--- Human input required: "); - var userInput = Console.ReadLine()?.Trim() ?? "Continue."; - await handle.RespondAsync(new { human_input = userInput }); - Console.WriteLine(); - break; - - case EventType.Done: - Console.WriteLine($"\nDone: {ev.Content ?? ev.Status}"); - break; - - case EventType.Error: - Console.WriteLine($"\nError: {ev.Content}"); - break; - } -} diff --git a/sdk/csharp/src/Agentspan/Tool.cs b/sdk/csharp/src/Agentspan/Tool.cs index c934d1475..e5dd0aef5 100644 --- a/sdk/csharp/src/Agentspan/Tool.cs +++ b/sdk/csharp/src/Agentspan/Tool.cs @@ -735,9 +735,12 @@ public static ToolDef Create( // If the LLM packed "gh repo list ..." into the command field, split it. // The first token is the executable; the rest prepend to args. - var cmdParts = rawCommand.Split(' ', StringSplitOptions.RemoveEmptyEntries); - var command = cmdParts[0]; - var prefixArgs = cmdParts.Length > 1 ? cmdParts[1..].ToList() : []; + // Tokenize honoring quotes so e.g. git commit -m "hello world" works. + var tokens = Tokenize(rawCommand); + if (tokens.Count == 0) + return (object)new Dictionary { ["status"] = "error", ["stderr"] = "No command provided." }; + var command = tokens[0]; + var prefixArgs = tokens.Count > 1 ? tokens.GetRange(1, tokens.Count - 1) : []; // Validate whitelist against the executable name (first token, basename) if (allowed.Count > 0) @@ -832,6 +835,56 @@ public static ToolDef Create( }, }; } + + /// + /// Tokenize a command line into argv, honoring single and double quotes. + /// LLMs frequently pass the whole command line as command + /// (e.g. gh repo list --limit 5). Falls back to plain whitespace + /// splitting if quotes are unbalanced. + /// + public static List Tokenize(string command) + { + var tokens = new List(); + var current = new System.Text.StringBuilder(); + var hasCurrent = false; + char? quote = null; + + foreach (var ch in command) + { + if (quote is not null) + { + if (ch == quote) quote = null; + else current.Append(ch); + } + else if (ch == '"' || ch == '\'') + { + quote = ch; + hasCurrent = true; + } + else if (char.IsWhiteSpace(ch)) + { + if (hasCurrent) + { + tokens.Add(current.ToString()); + current.Clear(); + hasCurrent = false; + } + } + else + { + current.Append(ch); + hasCurrent = true; + } + } + + if (quote is not null) + { + // Unbalanced quotes — fall back to naive whitespace split. + return command.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + if (hasCurrent) tokens.Add(current.ToString()); + return tokens; + } } // ── ToolRegistry ─────────────────────────────────────────── diff --git a/sdk/csharp/src/Agentspan/UserProxyAgent.cs b/sdk/csharp/src/Agentspan/UserProxyAgent.cs deleted file mode 100644 index 6d1bd32ae..000000000 --- a/sdk/csharp/src/Agentspan/UserProxyAgent.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. - -namespace Agentspan; - -/// How the user proxy requests human input. -public enum HumanInputMode -{ - /// Always pause for human input. - Always, - /// Pause only when the conversation would otherwise end. - Terminate, - /// Never pause — use 's default response. - Never, -} - -/// -/// Factory for creating a user-proxy agent — a human stand-in in multi-agent conversations. -/// -/// When it is the proxy's turn the workflow pauses with a event -/// and waits for input via . -/// -public static class UserProxyAgent -{ - /// - /// Create an agent that acts as a stand-in for a human user. - /// - /// Agent name (default "user"). - /// When to pause for human input (default Always). - /// Response when mode is . - /// LLM model (used only if the agent needs to generate a response). - /// System instructions (optional). - public static Agent Create( - string name = "user", - HumanInputMode humanInputMode = HumanInputMode.Always, - string defaultResponse = "Continue.", - string? model = null, - string? instructions = null) - { - var modeStr = humanInputMode switch - { - HumanInputMode.Always => "ALWAYS", - HumanInputMode.Terminate => "TERMINATE", - HumanInputMode.Never => "NEVER", - _ => "ALWAYS", - }; - - return new Agent(name) - { - Model = model ?? "openai/gpt-4o-mini", - Instructions = instructions - ?? "You represent the human user in this conversation. Relay the human's input exactly as provided.", - Metadata = new Dictionary - { - ["_agent_type"] = "user_proxy", - ["_human_input_mode"] = modeStr, - ["_default_response"] = defaultResponse, - }, - }; - } -} diff --git a/sdk/csharp/tests/Agentspan.OpenAI.Tests/CliToolTests.cs b/sdk/csharp/tests/Agentspan.OpenAI.Tests/CliToolTests.cs new file mode 100644 index 000000000..7fc4eeee7 --- /dev/null +++ b/sdk/csharp/tests/Agentspan.OpenAI.Tests/CliToolTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. + +using Agentspan; +using Xunit; + +namespace Agentspan.OpenAI.Tests; + +/// +/// Unit tests for — the command-line tokenizer +/// that lets the run_command tool accept a full command line packed into the +/// `command` field (as LLMs routinely produce). No LLM or server involved. +/// Parity target: Python cli_config.py (shlex) and TypeScript cli-config.ts. +/// +public class CliToolTests +{ + [Fact] + public void TokenizesBareExecutable() + { + Assert.Equal(new[] { "git" }, CliTool.Tokenize("git")); + } + + [Fact] + public void TokenizesFullCommandLine() + { + Assert.Equal( + new[] { "gh", "repo", "list", "--limit", "5" }, + CliTool.Tokenize("gh repo list --limit 5")); + } + + [Fact] + public void CollapsesRepeatedWhitespace() + { + Assert.Equal(new[] { "git", "status", "-s" }, CliTool.Tokenize("git status\t-s")); + } + + [Fact] + public void HonorsDoubleQuotedArguments() + { + // Naive Split(' ') would yield ["git","commit","-m","\"hello","world\""]. + Assert.Equal( + new[] { "git", "commit", "-m", "hello world" }, + CliTool.Tokenize("git commit -m \"hello world\"")); + } + + [Fact] + public void HonorsSingleQuotedArguments() + { + Assert.Equal( + new[] { "echo", "hello world" }, + CliTool.Tokenize("echo 'hello world'")); + } + + [Fact] + public void FallsBackToWhitespaceSplitOnUnbalancedQuotes() + { + // Unbalanced quote: don't throw, degrade to naive split. + Assert.Equal(new[] { "echo", "\"oops" }, CliTool.Tokenize("echo \"oops")); + } + + [Fact] + public void EmptyStringYieldsNoTokens() + { + Assert.Empty(CliTool.Tokenize("")); + } +} diff --git a/sdk/java/README.md b/sdk/java/README.md index 397abc5b9..808513dfa 100644 --- a/sdk/java/README.md +++ b/sdk/java/README.md @@ -14,7 +14,7 @@ Maven (`pom.xml`): ```xml - ai.agentspan + org.conductoross.conductor.ai java-sdk 0.1.0 @@ -23,7 +23,7 @@ Maven (`pom.xml`): Gradle (`build.gradle`): ```groovy -implementation 'ai.agentspan:java-sdk:0.1.0' +implementation 'org.conductoross.conductor.ai:java-sdk:0.1.0' ``` ### Spring Boot starter @@ -32,22 +32,22 @@ For Spring Boot apps, add the auto-configuration starter instead: ```xml - ai.agentspan + org.conductoross.conductor.ai java-sdk-spring 0.1.0 ``` ```groovy -implementation 'ai.agentspan:java-sdk-spring:0.1.0' +implementation 'org.conductoross.conductor.ai:java-sdk-spring:0.1.0' ``` ## Quick Start ```java -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; public class Main { public static void main(String[] args) { @@ -57,13 +57,17 @@ public class Main { .instructions("You are a helpful assistant.") .build(); - AgentResult result = Agentspan.run(agent, "What is the capital of France?"); - result.printResult(); - Agentspan.shutdown(); + // AgentRuntime is AutoCloseable — try-with-resources shuts down workers cleanly. + try (AgentRuntime runtime = new AgentRuntime()) { + AgentResult result = runtime.run(agent, "What is the capital of France?"); + result.printResult(); + } } } ``` +> In Spring, inject the auto-configured `AgentRuntime` bean instead of constructing one. + ## Configuration Set environment variables: @@ -78,8 +82,8 @@ export AGENTSPAN_LLM_MODEL=openai/gpt-4o Or configure programmatically: ```java -import ai.agentspan.AgentConfig; -import ai.agentspan.Agentspan; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; AgentConfig config = new AgentConfig( "http://localhost:6767/api", @@ -88,7 +92,7 @@ AgentConfig config = new AgentConfig( 100, // poll interval ms 5 // worker threads ); -Agentspan.configure(config); +AgentRuntime runtime = new AgentRuntime(config); ``` ## Tools @@ -96,8 +100,8 @@ Agentspan.configure(config); Define tools using the `@Tool` annotation: ```java -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; public class WeatherTools { @Tool(name = "get_weather", description = "Get weather for a city") @@ -125,7 +129,9 @@ Agent writer = Agent.builder().name("writer").model("openai/gpt-4o") // Sequential pipeline Agent pipeline = researcher.then(writer); -AgentResult result = Agentspan.run(pipeline, "Write about AI trends"); +try (AgentRuntime runtime = new AgentRuntime()) { + AgentResult result = runtime.run(pipeline, "Write about AI trends"); +} ``` ## Streaming diff --git a/sdk/java/build.gradle b/sdk/java/build.gradle index 1f0f29c82..bcb745b89 100644 --- a/sdk/java/build.gradle +++ b/sdk/java/build.gradle @@ -1,9 +1,11 @@ plugins { id 'java-library' + id 'jacoco' id 'com.vanniktech.maven.publish' version '0.34.0' + id 'com.diffplug.spotless' version '7.0.2' } -group = 'ai.agentspan' +group = 'org.conductoross.conductor' java { toolchain { @@ -22,6 +24,11 @@ ext { langchain4jVersion = '1.0.0' googleAdkVersion = '1.3.0' langgraph4jVersion = '1.6.0-beta5' + // Official Conductor Java client SDK (conductor-oss/java-sdk). Versioned + // separately from the server engine (engine = 3.30.2); wire-compatible with + // the 3.x task REST API, bundles the common DTOs, and provides native auth + // via io.orkes.conductor.client.ApiClient (key/secret → token). + conductorClientVersion = '5.0.1' } dependencies { @@ -33,11 +40,18 @@ dependencies { implementation "org.slf4j:slf4j-api:${slf4jVersion}" + // Official Conductor worker client — battle-tested task polling with + // automatic lease extension (heartbeat), backoff, and managed threads. + // `api` (not `implementation`): the Conductor ApiClient/ConductorClient is part + // of the SDK's public surface (AgentRuntime accepts/returns it), so downstream + // modules (spring) and user code must see it transitively. + api "org.conductoross:conductor-client:${conductorClientVersion}" + compileOnly "dev.langchain4j:langchain4j:${langchain4jVersion}" compileOnly "dev.langchain4j:langchain4j-open-ai:${langchain4jVersion}" // Google ADK + LangGraph4j are compileOnly so the SDK doesn't force users // onto them. The framework bridge classes only link at runtime when the - // user passes a native object to Agentspan.run/start/stream/deploy/serve — + // user passes a native object to runtime.run/start/stream/deploy/serve — // at which point that framework must obviously be on the user's classpath // already. compileOnly "com.google.adk:google-adk:${googleAdkVersion}" @@ -71,19 +85,58 @@ test { excludeTags 'e2e' } } + // For coverage runs: don't let a flaky e2e abort the JaCoCo report. + if (project.hasProperty('ignoreTestFailures')) { + ignoreFailures = true + } + // e2e suites are I/O-bound (LLM calls, docker) and use unique agent/task names, + // so they can safely run concurrently. 2 JVMs roughly halves wall-clock time. + // Unit tests are fast enough that parallelism doesn't matter for them. + if (project.hasProperty('e2e')) { + maxParallelForks = 3 + } +} + +spotless { + java { + target fileTree('.') { + include 'src/main/java/**/*.java', 'src/test/**/*.java', 'e2e/**/*.java', + 'spring/src/main/java/**/*.java', 'spring/src/test/**/*.java' + exclude '**/build/**' + } + palantirJavaFormat('2.50.0') + removeUnusedImports() + importOrder('java', 'javax', 'jakarta', 'org', 'com', 'dev', 'io') + trimTrailingWhitespace() + endWithNewline() + } } tasks.withType(Javadoc).configureEach { options.addStringOption('Xdoclint:none', '-quiet') } +// Code coverage. `jacocoTestReport` aggregates whatever ran in the `test` task — +// unit-only by default, or unit+e2e when run with `-Pe2e` (needs a live server). +jacoco { + toolVersion = '0.8.12' +} +jacocoTestReport { + dependsOn test + reports { + xml.required = true + csv.required = true + html.required = true + } +} + mavenPublishing { publishToMavenCentral(true) if (project.findProperty('signingInMemoryKey') != null) { signAllPublications() } - coordinates('ai.agentspan', 'java-sdk', project.version.toString()) + coordinates('org.conductoross.conductor', 'conductor-ai-sdk', project.version.toString()) pom { name = 'Agentspan Java SDK' diff --git a/sdk/java/docs/agent-client-api.md b/sdk/java/docs/agent-client-api.md new file mode 100644 index 000000000..c883ef89b --- /dev/null +++ b/sdk/java/docs/agent-client-api.md @@ -0,0 +1,339 @@ +# AgentClient — Control-Plane API Reference + +`AgentClient` is the Java SDK's interface to the Agentspan server's proprietary agent control-plane (`/api/agent/*`). Strictly scoped to five endpoints — compile, deploy, start, status, respond. Standard Conductor endpoints (`/api/workflow/*`, `/api/tasks`, etc.) are handled by the Conductor SDK's own typed clients (`WorkflowClient`, `TaskClient`, `MetadataClient`). + +Every request goes through the shared `ConductorClient`'s native HTTP + auth + serialization layer. No hand-rolled HTTP. `ConductorClientException` is mapped to agentspan's `AgentAPIException` / `AgentNotFoundException`. + +## Methods + +| Method | HTTP | Java input type | Java return type | Description | +|---|---|---|---|---| +| [`compileAgent`](#compileagent) | `POST /api/agent/compile` | `AgentRequest` | `CompileResponse` | Compile to Conductor workflow def — no side effects | +| [`deployAgent`](#deployagent) | `POST /api/agent/deploy` | `AgentRequest` | `StartResponse` | Register workflow def without starting | +| [`startAgent`](#startagent) | `POST /api/agent/start` | `AgentRequest` | `StartResponse` | Compile + register + start execution | +| [`getAgentStatus`](#getagentstatus) | `GET /api/agent/{id}/status` | path: `executionId` | `AgentStatusResponse` | Poll execution status; includes HITL pending-tool | +| [`respond`](#respond) | `POST /api/agent/{id}/respond` | `RespondBody` | `void` | Resume a paused HITL task | + +--- + +## AgentRequest + +Input to `compileAgent`, `deployAgent`, and `startAgent`. Holds a single `Agent` field — no `agentConfig`/`rawConfig` duplication. A custom `Serializer` writes the correct JSON shape based on the `Framework` discriminator. + +```java +// Native agent +AgentRequest.nativeAgent(agent).build() + +// Framework-backed agent — Framework enum, not a String +AgentRequest.frameworkAgent(Framework.OPENAI, agent).build() +AgentRequest.frameworkAgent(Framework.LANGCHAIN, agent).build() + +// With execution fields (for /start only) +AgentRequest.nativeAgent(agent) + .prompt("What is the capital of France?") + .sessionId("session-abc") + .runId("a1b2c3...") // per-execution domain UUID for stateful agents + .staticPlan(plan) // Plan — Serializer calls plan.toJson() internally + .build() +``` + +**`AgentRequest.Serializer` wire output:** + +| When | JSON emitted | +|---|---| +| `framework == null` (native) | `"agentConfig": AgentConfigSerializer.serialize(agent)` | +| `framework != null` (framework-backed) | `"framework": fw.wireValue(), "rawConfig": AgentConfigSerializer.serialize(agent)` | + +**Field mapping to server `StartRequest`:** + +| `AgentRequest` field | Java type | JSON key(s) written by Serializer | Server `StartRequest` field | Used by | +|---|---|---|---|---| +| `agent` | `Agent` | `"agentConfig"` or `"rawConfig"` (see above) | `agentConfig` / `rawConfig` | all | +| `framework` | `Framework` | `"framework"` (only when non-null) | `framework` | framework agents | +| `prompt` | `String` | `"prompt"` | `prompt` | start only | +| `sessionId` | `String` | `"sessionId"` | `sessionId` | start (stateful) | +| `runId` | `String` | `"runId"` | `runId` | start (stateful isolation) | +| `staticPlan` | `Plan` | `"static_plan"` (Serializer calls `plan.toJson()`) | `staticPlan` (`@JsonProperty("static_plan")`) | start (PLAN_EXECUTE) | +| `media` | `List` | `"media"` | `media` | start (multi-modal) | +| `context` | `Map` | `"context"` | `context` | start | +| `idempotencyKey` | `String` | `"idempotencyKey"` | `idempotencyKey` | start | +| `credentials` | `List` | `"credentials"` | `credentials` | compile / start | +| `timeoutSeconds` | `Integer` | `"timeoutSeconds"` | `timeoutSeconds` | compile / start | + +Null fields are never written — the `Serializer` uses explicit null-checks, not `@JsonInclude`. + +**`Framework` enum** — all seven values map 1-to-1 with the server's normalizer registry: + +| Enum constant | Wire value | Server normalizer | +|---|---|---| +| `Framework.OPENAI` | `"openai"` | `OpenAINormalizer` | +| `Framework.GOOGLE_ADK` | `"google_adk"` | `GoogleADKNormalizer` | +| `Framework.LANGCHAIN` | `"langchain"` | `LangChainNormalizer` | +| `Framework.LANGGRAPH` | `"langgraph"` | `LangGraphNormalizer` | +| `Framework.SKILL` | `"skill"` | `SkillNormalizer` | +| `Framework.VERCEL_AI` | `"vercel_ai"` | `VercelAINormalizer` | +| `Framework.CLAUDE_AGENT_SDK` | `"claude_agent_sdk"` | `ClaudeAgentSdkNormalizer` | + +`AgentRuntime` resolves `agent.getFramework()` → `Framework` via `Framework.of(String)` (returns `Optional.empty()` for unrecognised strings, routing them through the native path). + +**Structural proof — `static_plan` key:** +The server field is `staticPlan` annotated `@JsonProperty("static_plan")`. The `Serializer` writes `gen.writeObjectField("static_plan", ...)` — both sides agree on the JSON key. + +--- + +## RespondBody + +Input to `respond`. Provides factory methods for the three common patterns; arbitrary extra fields are flattened to the top level via `@JsonAnyGetter`. + +```java +RespondBody.approve() // { "approved": true } +RespondBody.approve("Looks good") // { "approved": true, "reason": "Looks good" } +RespondBody.reject("Needs review") // { "approved": false, "reason": "Needs review" } +RespondBody.of(Map.of("selected", "writer")) // { "selected": "writer" } ← MANUAL strategy +``` + +**Used by `AgentHandle`:** + +| `AgentHandle` method | `RespondBody` factory | Wire JSON | +|---|---|---| +| `handle.approve()` | `RespondBody.approve()` | `{ "approved": true }` | +| `handle.approve(comment)` | `RespondBody.approve(comment)` | `{ "approved": true, "reason": "..." }` | +| `handle.reject(reason)` | `RespondBody.reject(reason)` | `{ "approved": false, "reason": "..." }` | +| `handle.respond(map)` | `RespondBody.of(map)` | the map at the top level | + +--- + +## compileAgent + +Compile an agent into a Conductor workflow definition. No workflow is registered or executed. + +Used by `AgentRuntime.plan(agent)`. + +**HTTP:** `POST /api/agent/compile` + +### Request body — `AgentRequest` + +```java +// AgentRuntime builds this via agentRequest(agent): +AgentRequest.nativeAgent(agent).build() +// or, for framework agents — uses Framework enum, not a raw String: +AgentRequest.frameworkAgent(Framework.OPENAI, agent).build() +``` + +Native agent wire shape (produced by `AgentRequest.Serializer` calling `AgentConfigSerializer.serialize(agent)`): +```json +{ "agentConfig": { "name": "my_agent", "model": "openai/gpt-4o-mini", "strategy": "handoff", ... } } +``` + +Framework agent wire shape: +```json +{ "framework": "openai", "rawConfig": { "name": "my_agent", "model": "openai/gpt-4o-mini", "tools": [...] } } +``` + +### Response — `CompileResponse` + +```json +{ "workflowDef": { "name": "my_agent", "version": 1, "tasks": [...] }, "requiredWorkers": ["my_tool_a"] } +``` + +| Field | Getter | Type | Description | +|---|---|---|---| +| `workflowDef` | `getWorkflowDef()` | `Map` | Full Conductor workflow definition. | +| `requiredWorkers` | `getRequiredWorkers()` | `List` | Task type names the SDK must register local workers for. | + +**How the SDK uses it:** `AgentRuntime.plan(agent)` returns the `CompileResponse` directly. + +--- + +## deployAgent + +Compile and register the workflow definition on the server without starting an execution. Idempotent. + +Used by `AgentRuntime.deploy(Agent...)`. + +**HTTP:** `POST /api/agent/deploy` + +### Request body — `AgentRequest` + +Same as `compileAgent` — agent definition only, no `prompt`. + +### Response — `StartResponse` + +```json +{ "agentName": "my_agent", "requiredWorkers": ["my_tool_a"] } +``` + +| Field | Getter | Type | Description | +|---|---|---|---| +| `agentName` | `getAgentName()` | `String` | The registered workflow name on the server. | +| `requiredWorkers` | `getRequiredWorkers()` | `List` | Task type names the SDK must have workers running for. | +| `executionId` | `getExecutionId()` | `String` | Always `null` for deploy — no execution was started. | + +**How the SDK uses it:** `AgentRuntime.deploy()` reads `resp.getAgentName()` and wraps it in `DeploymentInfo`. + +--- + +## startAgent + +Compile, register, and start a workflow execution in one call. + +Used by `AgentRuntime.startAsync(agent, prompt, plan)`. + +**HTTP:** `POST /api/agent/start` + +### Request body — `AgentRequest` + +```json +{ + "agentConfig": { ... }, + "prompt": "What is the capital of France?", + "sessionId": "session-abc", + "runId": "a1b2c3d4e5f6...", + "static_plan": { "steps": [...] } +} +``` + +### Response — `StartResponse` + +```json +{ "executionId": "a3f92b1c-8e4d-4b7a-9c2e-1d5f3a8e6b02", "agentName": "my_agent", "requiredWorkers": ["my_tool_a"] } +``` + +| Field | Getter | Type | Description | +|---|---|---|---| +| `executionId` | `getExecutionId()` | `String` | Conductor workflow ID. `@JsonAlias` handles legacy keys (`workflowId`, `id`, `correlationId`). | +| `agentName` | `getAgentName()` | `String` | The registered workflow name. | +| `requiredWorkers` | `getRequiredWorkers()` | `List` | Task type names the SDK must have workers polling before the agent can progress. | + +**How the SDK uses it:** `AgentRuntime.startAsync()` reads `response.getExecutionId()` and passes it to `new AgentHandle(executionId, agentClient, workflowClient)`. + +--- + +## getAgentStatus + +Poll the current status of a running or completed execution. + +Used by `AgentHandle.waitForResult()` and `AgentHandle.waitUntilWaiting()`. + +**HTTP:** `GET /api/agent/{executionId}/status` + +### Response — `AgentStatusResponse` + +```json +{ "executionId": "...", "status": "COMPLETED", "isComplete": true, "isRunning": false, "output": { ... } } +``` + +HITL paused: +```json +{ "status": "RUNNING", "isWaiting": true, "pendingTool": { "taskRefName": "...", "tool_name": "...", "parameters": { ... } } } +``` + +**`AgentStatusResponse` fields:** + +| JSON field | Getter | Type | Source | Description | +|---|---|---|---|---| +| `executionId` | `getExecutionId()` | `String` | path param | — | +| `status` | `getStatus()` | `String` | `workflow.getStatus().name()` | `RUNNING`, `COMPLETED`, `FAILED`, `TERMINATED`, `TIMED_OUT`, `PAUSED` | +| `isComplete` | `isComplete()` | `boolean` | `workflow.getStatus().isTerminal()` | `true` for all terminal statuses | +| `isRunning` | `isRunning()` | `boolean` | `status == RUNNING` | — | +| `output` | `getOutput()` | `Map` | `workflow.getOutput()` | Only present when `isComplete() == true` | +| `reasonForIncompletion` | `getReasonForIncompletion()` | `String` | `workflow.getReasonForIncompletion()` | Only present on non-COMPLETED terminal status | +| `isWaiting` | `isWaiting()` | `boolean` | HUMAN task IN_PROGRESS | `true` when a HITL task is paused | +| `pendingTool` | `getPendingTool()` | `PendingTool` | HUMAN task inputData | Only when `isWaiting() == true` | + +**`PendingTool` fields:** + +| JSON field | Getter | Type | Description | +|---|---|---|---| +| `taskRefName` | `getTaskRefName()` | `String` | Conductor task reference name. | +| `tool_name` | `getToolName()` | `String` | Logical tool name shown to the human. | +| `parameters` | `getParameters()` | `Map` | Args the agent passed to the tool. | +| `response_schema` | `getResponseSchema()` | `Object` | JSON Schema the response must conform to (optional). | +| `response_ui_schema` | `getResponseUiSchema()` | `Object` | UI rendering hints (optional). | + +--- + +## respond + +Resume a paused HITL execution. + +Used by `AgentHandle.approve()`, `.reject()`, `.respond(Map)` and `AgentStream.approve()`, `.reject()`. + +**HTTP:** `POST /api/agent/{executionId}/respond` + +### Request body — `RespondBody` + +```json +{ "approved": true } +{ "approved": false, "reason": "Needs review" } +{ "selected": "writer" } +``` + +### Response + +`void` — returns nothing. Throws `AgentAPIException` if no pending HUMAN task exists. + +--- + +## WorkflowClient usage + +Raw workflow data (`GET /api/workflow/{id}`) is fetched via the standard Conductor `WorkflowClient` — not `AgentClient`. `AgentClient` owns only `/api/agent/*`. + +`WorkflowClient.getWorkflow(id, true)` is called inside `AgentHandle.buildResult()` **once**, after `getAgentStatus` returns terminal, to walk the typed `Workflow`/`Task` objects and compute: + +- **Token usage** — `LLM_CHAT_COMPLETE` task `outputData`: `promptTokens`, `completionTokens`, `tokenUsed` → `AgentResult.getTokenUsage()` +- **Tool calls** — worker tasks whose `referenceTaskName` starts with `call_` → `AgentResult.getToolCalls()` + +Fires automatically inside `run()` / `waitForResult()`. Callers never invoke it directly. + +--- + +## AgentConfig (request field) + +The agent definition serialized under the `agentConfig` key by `AgentConfigSerializer`. + +| Field | Type | Description | +|---|---|---| +| `name` | `String` | Agent/workflow name. | +| `model` | `String` | `"provider/model"` e.g. `"openai/gpt-4o-mini"`. | +| `instructions` | `String \| Object` | System prompt or `PromptTemplateRef`. | +| `tools` | `List` | Tool definitions. | +| `agents` | `List` | Sub-agents (for multi-agent strategies). | +| `strategy` | `String` | `"handoff"` (default), `"sequential"`, `"parallel"`, `"router"`, `"swarm"`, `"round_robin"`, `"random"`, `"plan_execute"`, `"manual"`. | +| `router` | `AgentConfig \| WorkerRef` | For `"router"` strategy. | +| `guardrails` | `List` | Input/output guardrails. | +| `maxTurns` | `int` | Default `100`. | +| `maxTokens` | `Integer` | LLM `max_tokens`. | +| `temperature` | `Double` | LLM temperature. | +| `timeoutSeconds` | `int` | Execution timeout. | +| `credentials` | `List` | Credential names injected at runtime. | +| `outputType` | `OutputTypeConfig` | Structured output definition. | +| `termination` | `TerminationConfig` | Early termination condition. | +| `handoffs` | `List` | Swarm handoff triggers. | +| `callbacks` | `List` | Before/after model callbacks. | +| `codeExecution` | `CodeExecutionConfig` | Local code execution settings. | +| `cliConfig` | `CliConfig` | CLI command execution settings. | +| `planner` | `AgentConfig` | `PLAN_EXECUTE`: agent that produces the plan. | +| `fallback` | `AgentConfig` | `PLAN_EXECUTE`: agent used when the plan fails. | +| `plannerContext` | `List` | `PLAN_EXECUTE`: text/URL context appended to the planner's prompt. | +| `synthesize` | `Boolean` | Append a synthesis step after parallel sub-agents. | +| `includeContents` | `String` | `"none"` = fresh context; absent = inherit parent context. | +| `baseUrl` | `String` | Per-agent LLM provider base URL override. | +| `metadata` | `Map` | Arbitrary metadata stored with the workflow definition. | +| `framework` | `String` | Framework ID — set by SDK bridges, not by callers directly. | + +### ToolConfig + +| Field | Type | Default | Description | +|---|---|---|---| +| `name` | `String` | | Tool name shown to the LLM. | +| `description` | `String` | | Tool description. | +| `inputSchema` | `Map` | | JSON Schema for tool parameters. | +| `outputSchema` | `Map` | | JSON Schema for tool return value. | +| `toolType` | `String` | `"worker"` | `"worker"`, `"http"`, `"mcp"`, `"human"`, `"generate_image"`, `"generate_audio"`, `"generate_pdf"`, `"rag_search"`, `"pull_workflow_messages"`. | +| `approvalRequired` | `boolean` | `false` | Pause for human approval before executing. | +| `timeoutSeconds` | `Integer` | | Per-tool execution timeout. | +| `maxCalls` | `Integer` | | Maximum invocations per run. | +| `config` | `Map` | | Type-specific config: `url`/`method`/`headers` for HTTP; `server_url` for MCP. | +| `guardrails` | `List` | | Tool-level guardrails. | +| `stateful` | `boolean` | `false` | Register worker under a per-execution domain (prevents cross-instance task stealing). | diff --git a/sdk/java/docs/agent-runtime-api.md b/sdk/java/docs/agent-runtime-api.md new file mode 100644 index 000000000..062558bb3 --- /dev/null +++ b/sdk/java/docs/agent-runtime-api.md @@ -0,0 +1,384 @@ +# AgentRuntime — API Reference + +`AgentRuntime` is the primary entry point for the Agentspan Java SDK. It manages the connection to the Agentspan server, registers local tool workers, and exposes every operation for running, streaming, deploying, and serving agents. + +Implements `AutoCloseable` — always use try-with-resources or call `shutdown()` explicitly. + +```java +try (AgentRuntime runtime = new AgentRuntime()) { + AgentResult result = runtime.run(agent, "Hello!"); + System.out.println(result.getOutput()); +} +``` + +--- + +## Method summary + +| Method | Returns | Description | +|---|---|---| +| [`run`](#run) | `AgentResult` | Execute an agent synchronously | +| [`runAsync`](#runasync) | `CompletableFuture` | Execute an agent asynchronously | +| [`start`](#start) | `AgentHandle` | Fire-and-forget; returns a handle to poll/approve | +| [`startAsync`](#startasync) | `CompletableFuture` | Async fire-and-forget | +| [`stream`](#stream) | `AgentStream` | Execute and iterate events as they arrive | +| [`streamAsync`](#streamasync) | `CompletableFuture` | Async event stream | +| [`plan`](#plan) | `CompileResponse` | Compile without executing | +| [`deploy`](#deploy) | `List` | Register workflow definition(s) | +| [`deployAsync`](#deployasync) | `CompletableFuture>` | Async deploy | +| [`serve`](#serve) | `void` | Long-running worker mode (blocks) | +| [`resume`](#resume) | `AgentHandle` | Re-attach to an existing execution | +| [`resumeAsync`](#resumeasync) | `CompletableFuture` | Async re-attach | +| [`schedules`](#schedules) | `Schedules` | Access the scheduling API | +| [`shutdown`](#shutdown) | `void` | Stop workers and release HTTP connections | + +--- + +## Constructors + +```java +new AgentRuntime() +``` +Reads server URL and auth from environment, worker tuning from environment. + +```java +new AgentRuntime(AgentConfig config) +``` +Reads server URL and auth from environment; explicit worker tuning. + +```java +new AgentRuntime(ApiClient conductorClient) +``` +Explicit server connection; worker tuning from environment. + +```java +new AgentRuntime(ApiClient conductorClient, AgentConfig config) +``` +Fully explicit — the canonical constructor all others delegate to. + +### Environment variables + +| Variable | Default | Description | +|---|---|---| +| `AGENTSPAN_SERVER_URL` | `http://localhost:6767` | Agentspan server base URL | +| `AGENTSPAN_AUTH_KEY` | _(none)_ | API key (optional) | +| `AGENTSPAN_AUTH_SECRET` | _(none)_ | API secret (optional) | +| `AGENTSPAN_WORKER_POLL_INTERVAL` | `100` | Worker poll interval (ms) | +| `AGENTSPAN_WORKER_THREADS` | `1` | Worker thread count | + +--- + +## ApiClient factories + +Build an `ApiClient` to pass to the `AgentRuntime(ApiClient)` constructor. The `ApiClient` owns server URL, auth, and HTTP timeouts. + +```java +// From environment (same as the no-arg constructor uses internally) +ApiClient client = AgentRuntime.clientFromEnv(); + +// Unauthenticated — local dev +ApiClient client = AgentRuntime.client("http://localhost:6767"); + +// Key/secret auth +ApiClient client = AgentRuntime.client("http://myserver:6767", "key", "secret"); +``` + +Default timeouts: `connectTimeout=10s`, `readTimeout=30s`, `writeTimeout=30s`. The `/api` base path is appended automatically. + +--- + +## run + +Execute an agent and block until it completes. The most common operation. + +```java +AgentResult result = runtime.run(agent, "What is the capital of France?"); +System.out.println(result.getOutput()); +System.out.println(result.getStatus()); // AgentStatus.COMPLETED +System.out.println(result.getTokenUsage()); // TokenUsage{prompt=312, completion=47, total=359} +``` + +**Overloads:** + +```java +AgentResult run(Agent agent, String prompt) + +// For PLAN_EXECUTE strategy — bypasses the planner LLM entirely +AgentResult run(Agent agent, String prompt, Plan plan) +``` + +**What happens internally:** +1. Workers for the agent's tools are registered with the Conductor task runner. +2. `POST /api/agent/start` — server compiles, registers, and starts the workflow. +3. Polls `GET /api/agent/{id}/status` every 2 seconds until terminal. +4. On completion, calls `GET /api/workflow/{id}` once to aggregate token usage and tool calls into the `AgentResult`. + +**Returns `AgentResult`:** + +| Method | Type | Description | +|---|---|---| +| `getOutput()` | `Object` | Final LLM output (String or structured object) | +| `getStatus()` | `AgentStatus` | `COMPLETED`, `FAILED`, `TERMINATED`, `TIMED_OUT` | +| `getExecutionId()` | `String` | Conductor workflow ID | +| `getTokenUsage()` | `TokenUsage` | Aggregated `promptTokens`, `completionTokens`, `totalTokens` | +| `getToolCalls()` | `List>` | All tool invocations: `{name, args, result}` | +| `getEvents()` | `List` | Full event log (populated by streaming paths) | +| `getError()` | `String` | Failure/termination reason when `status != COMPLETED` | +| `isSuccess()` | `boolean` | `true` when `status == COMPLETED` | + +--- + +## runAsync + +Non-blocking variant of `run`. Uses the common `ForkJoinPool`. + +```java +CompletableFuture runAsync(Agent agent, String prompt) +CompletableFuture runAsync(Agent agent, String prompt, Plan plan) +``` + +```java +// Run multiple agents concurrently +List> futures = prompts.stream() + .map(p -> runtime.runAsync(agent, p)) + .toList(); +CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); +``` + +`AgentRuntime` is thread-safe — share one instance across threads. + +--- + +## start + +Fire-and-forget: registers workers, starts the execution, and returns immediately with an `AgentHandle`. Does not wait for completion. + +```java +AgentHandle handle = runtime.start(agent, "Deploy version 2.1 to production"); +String executionId = handle.getExecutionId(); + +// Poll later +AgentResult result = handle.waitForResult(); + +// Or approve a HITL step +handle.approve("Approved by Alice"); +handle.reject("Needs more testing"); + +// Respond for MANUAL strategy +handle.respond(Map.of("selected", "writer_agent")); +``` + +**`AgentHandle` methods:** + +| Method | Description | +|---|---| +| `getExecutionId()` | Conductor workflow ID | +| `waitForResult()` | Block until completion (default 10-min timeout) | +| `waitForResult(long timeoutMs, long pollMs)` | Block with explicit timeout | +| `waitUntilWaiting(long timeoutMs)` | Block until a HITL task is paused | +| `isWaiting()` | `true` if a HITL task is currently paused | +| `approve()` | Resume with `{"approved": true}` | +| `approve(String comment)` | Resume with approval + comment | +| `reject(String reason)` | Resume with `{"approved": false, "reason": ...}` | +| `respond(Map)` | Send arbitrary response (MANUAL strategy, custom schemas) | + +--- + +## startAsync + +```java +CompletableFuture startAsync(Agent agent, String prompt) +CompletableFuture startAsync(Agent agent, String prompt, Plan plan) +``` + +--- + +## stream + +Execute and iterate over events as they are emitted by the server via SSE. Blocks the calling thread during iteration. + +```java +try (AgentStream stream = runtime.stream(agent, "Tell me a story")) { + for (AgentEvent event : stream) { + switch (event.getType()) { + case MESSAGE -> System.out.print(event.getContent()); + case TOOL_CALL -> System.out.println("→ " + event.getToolName() + "(" + event.getArgs() + ")"); + case TOOL_RESULT -> System.out.println("← " + event.getResult()); + case DONE -> { /* stream ended */ } + } + } +} +// After iteration, stream.waitForResult() returns the completed AgentResult +``` + +`AgentStream` implements `Iterable` and `AutoCloseable`. + +**`AgentEvent` fields:** + +| Method | Type | Description | +|---|---|---| +| `getType()` | `EventType` | `MESSAGE`, `TOOL_CALL`, `TOOL_RESULT`, `THINKING`, `WAITING`, `HANDOFF`, `ERROR`, `DONE` | +| `getContent()` | `String` | Message text or thinking content | +| `getToolName()` | `String` | Tool name for `TOOL_CALL`/`TOOL_RESULT` | +| `getArgs()` | `Map` | Tool arguments for `TOOL_CALL` | +| `getResult()` | `Object` | Tool result for `TOOL_RESULT` | +| `getExecutionId()` | `String` | Execution that emitted this event | + +**HITL approval from a stream** — use the event-targeted overloads so sub-execution approvals route correctly: + +```java +for (AgentEvent event : stream) { + if (event.getType() == EventType.WAITING) { + stream.approve(event); // targets event.getExecutionId() + // or: stream.reject(event, "reason") + } +} +``` + +--- + +## streamAsync + +```java +CompletableFuture streamAsync(Agent agent, String prompt) +``` + +--- + +## plan + +Compile the agent into a Conductor workflow definition without registering or starting anything. Useful for inspecting the workflow shape, CI/CD validation, or pre-warming the server cache. + +```java +CompileResponse compile = runtime.plan(agent); + +Map workflowDef = compile.getWorkflowDef(); +List requiredWorkers = compile.getRequiredWorkers(); +``` + +Delegates to `AgentClient.compileAgent` → `POST /api/agent/compile`. + +--- + +## deploy + +Register workflow definition(s) on the server without starting an execution. Idempotent — safe to call on every application startup. + +```java +// One or more agents +List infos = runtime.deploy(agentA, agentB); +infos.forEach(i -> System.out.println(i.getRegisteredName())); + +// With schedules — deploys the agent and reconciles its cron schedules +Schedule daily = Schedule.builder().name("daily").cron("0 9 * * *").build(); +runtime.deploy(agent, List.of(daily)); +``` + +**`DeploymentInfo` fields:** `getRegisteredName()` (server workflow name), `getAgentName()` (SDK agent name). + +--- + +## deployAsync + +```java +CompletableFuture> deployAsync(Agent... agents) +``` + +--- + +## serve + +Register workers and block indefinitely, polling for tasks from the Conductor server. Designed for long-running worker processes. + +```java +// Blocks until the process is killed (SIGTERM triggers graceful shutdown) +runtime.serve(agentA, agentB); +``` + +A JVM shutdown hook calls `workerManager.stop()` on SIGTERM. Unlike `run()`, `serve()` does not start any executions — it just makes the agent's workers available for tasks the server dispatches. + +--- + +## resume + +Re-attach to a running or paused execution that was started in a previous process. Re-registers the agent's workers so they can continue serving tasks. + +```java +AgentHandle handle = runtime.resume("a3f92b1c-...", agent); +AgentResult result = handle.waitForResult(); +``` + +Useful for crash recovery or reconnecting after a planned restart. + +--- + +## resumeAsync + +```java +CompletableFuture resumeAsync(String executionId, Agent agent) +``` + +--- + +## schedules + +Access the scheduling API lazily (created on first call, shared thereafter). + +```java +Schedules schedules = runtime.schedules(); + +schedules.list("my_agent"); // List +schedules.runNow(schedules.get("my_agent-daily")); // trigger immediately (takes ScheduleInfo) +schedules.pause("my_agent-daily"); +schedules.resume("my_agent-daily"); +schedules.delete("my_agent-daily"); +``` + +See [Scheduling concepts](concepts/scheduling.md) for the full `Schedules` API. + +--- + +## shutdown + +Stop all worker threads, drain in-flight tasks, and release HTTP connections (OkHttp connection pool + dispatcher thread pool). + +```java +runtime.shutdown(); +// equivalent: +runtime.close(); // AutoCloseable — called automatically by try-with-resources +``` + +Without explicit shutdown, OkHttp's thread pool keeps threads alive for ~60s after the last request. In tests or short-lived processes, always close the runtime. + +--- + +## AgentConfig + +Worker-runner tuning. **Does not hold server URL or auth** — those are on `ApiClient`. + +```java +new AgentConfig() // defaults: 100ms poll, 1 thread +new AgentConfig(pollMs, threads) // explicit +AgentConfig.fromEnv() // reads AGENTSPAN_WORKER_* env vars +``` + +| Parameter | Env var | Default | Description | +|---|---|---|---| +| `workerPollIntervalMs` | `AGENTSPAN_WORKER_POLL_INTERVAL` | `100` | How often workers poll for tasks (ms). 100ms is fast for dev; raise to 500–1000ms in production to reduce server load. | +| `workerThreadCount` | `AGENTSPAN_WORKER_THREADS` | `1` | Thread pool size. The actual pool is `max(configured, numWorkerTypes)` so every task type gets at least one thread. | + +--- + +## Thread safety + +`AgentRuntime` is thread-safe. Share one instance across all threads in an application: + +```java +// Application lifecycle — create once +private static final AgentRuntime RUNTIME = new AgentRuntime(); + +// Shut down on application exit +Runtime.getRuntime().addShutdownHook(new Thread(RUNTIME::shutdown)); +``` + +Do not create one `AgentRuntime` per request — each instance owns its own OkHttp connection pool and worker thread pool. diff --git a/sdk/java/docs/agent-schema.json b/sdk/java/docs/agent-schema.json new file mode 100644 index 000000000..e2c680cca --- /dev/null +++ b/sdk/java/docs/agent-schema.json @@ -0,0 +1,317 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://agentspan.ai/schemas/agent-config.schema.json", + "title": "Agentspan AgentConfig", + "description": "Canonical wire contract for the agent configuration that SDKs serialize and POST to the server (under the `agentConfig` key of the start/compile request). Mirrors the server-side `AgentConfig` model. Convention: camelCase keys, `@JsonInclude(NON_NULL)` — absent means unset. Recursive: `agents`, `planner`, `fallback`, `router` nest a full AgentConfig.", + "type": "object", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_-]*$", + "description": "Agent name. Required." + }, + "description": { + "type": "string", + "description": "Human-readable description. Set by the platform/UI, not the SDKs." + }, + "model": { + "type": ["string", "null"], + "description": "\"provider/model\" identifier. Omitted/null for external agents." + }, + "external": { + "type": "boolean", + "description": "True when the agent has no model and is driven externally." + }, + "baseUrl": { + "type": "string", + "description": "Per-agent LLM provider endpoint override." + }, + "instructions": { + "description": "System prompt: a plain string, or a structured prompt-template reference.", + "oneOf": [ + { "type": "string" }, + { "type": "null" }, + { "$ref": "#/$defs/promptTemplate" } + ] + }, + "introduction": { + "type": "string", + "description": "Self-introduction prepended in multi-agent discussions." + }, + "tools": { + "type": "array", + "items": { "$ref": "#/$defs/tool" } + }, + "agents": { + "type": "array", + "items": { "$ref": "#" }, + "description": "Sub-agents (recursive). Requires a strategy." + }, + "strategy": { + "type": ["string", "null"], + "enum": ["handoff", "sequential", "parallel", "router", "round_robin", "random", "swarm", "manual", "plan_execute", null], + "description": "Multi-agent orchestration strategy. Null/absent for a single agent." + }, + "router": { + "description": "ROUTER strategy router: a nested agent or a worker-task reference.", + "oneOf": [ + { "$ref": "#" }, + { "$ref": "#/$defs/workerRef" } + ] + }, + "guardrails": { + "type": "array", + "items": { "$ref": "#/$defs/guardrail" } + }, + "maxTurns": { "type": "integer", "description": "Max agent turns. SDK default 25; server default 100." }, + "maxTokens": { "type": "integer", "description": "LLM completion token cap." }, + "temperature": { "type": "number", "description": "Sampling temperature." }, + "timeoutSeconds": { "type": "integer", "description": "Run timeout. 0 → server default." }, + "reasoningEffort": { + "type": "string", + "enum": ["minimal", "low", "medium", "high"], + "description": "OpenAI reasoning models only. Ignored by other models." + }, + "contextWindowBudget": { "type": "integer", "description": "Token threshold for proactive context condensation." }, + "thinkingConfig": { "$ref": "#/$defs/thinkingConfig" }, + "memory": { "$ref": "#/$defs/memory" }, + "termination": { "$ref": "#/$defs/termination" }, + "outputType": { "$ref": "#/$defs/outputType" }, + "handoffs": { + "type": "array", + "items": { "$ref": "#/$defs/handoff" } + }, + "allowedTransitions": { + "type": "object", + "additionalProperties": { "type": "array", "items": { "type": "string" } }, + "description": "SWARM: agentName → list of agents it may transfer to." + }, + "callbacks": { + "type": "array", + "items": { "$ref": "#/$defs/callback" } + }, + "gate": { "$ref": "#/$defs/gate" }, + "stopWhen": { "$ref": "#/$defs/workerRef" }, + "enablePlanning": { "type": "boolean", "description": "Prepends a plan-first preamble to the system prompt." }, + "planner": { "$ref": "#", "description": "PLAN_EXECUTE planner slot (nested agent)." }, + "fallback": { "$ref": "#", "description": "PLAN_EXECUTE fallback slot (nested agent)." }, + "fallbackMaxTurns": { "type": "integer" }, + "plannerContext": { + "type": "array", + "items": { "$ref": "#/$defs/plannerContextEntry" } + }, + "planSource": { + "type": "object", + "additionalProperties": true, + "description": "Static plan JSON for PLAN_EXECUTE. (Python emits here; the Java SDK sends the static plan via the request wrapper's `static_plan` instead.)" + }, + "synthesize": { "type": "boolean", "description": "Emitted only when false (default true)." }, + "stateful": { "type": "boolean", "description": "Emitted only when true; triggers per-execution domain isolation." }, + "sessionId": { + "type": "string", + "description": "Session id. The Java SDK echoes it here and in the request wrapper; the server reads it from the wrapper." + }, + "includeContents": { + "type": "string", + "description": "\"none\" = fresh context; absent = inherit parent context." + }, + "requiredTools": { "type": "array", "items": { "type": "string" } }, + "prefillTools": { + "type": "array", + "items": { "$ref": "#/$defs/prefillTool" } + }, + "credentials": { "type": "array", "items": { "type": "string" } }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Arbitrary key-values stored with the workflow definition." + }, + "localCodeExecution": { + "type": "boolean", + "description": "Java builder flag; serialized into `codeExecution`. Not a distinct wire field on its own." + }, + "codeExecution": { "$ref": "#/$defs/codeExecution" }, + "cliConfig": { "$ref": "#/$defs/cliConfig" }, + "maskedFields": { "type": "array", "items": { "type": "string" } } + }, + "$defs": { + "promptTemplate": { + "type": "object", + "required": ["type", "name"], + "additionalProperties": true, + "properties": { + "type": { "type": "string", "const": "prompt_template" }, + "name": { "type": "string" }, + "variables": { "type": ["object", "null"], "additionalProperties": true }, + "version": { "type": ["integer", "null"] } + } + }, + "tool": { + "type": "object", + "additionalProperties": true, + "description": "Tool definition. `config` is a freeform map carrying type-specific settings (url, serverUrl, credentials, …), so unknown keys are permitted.", + "properties": { + "name": { "type": "string" }, + "description": { "type": "string" }, + "inputSchema": { "type": "object", "additionalProperties": true }, + "outputSchema": { "type": "object", "additionalProperties": true }, + "toolType": { "type": "string", "description": "worker | http | mcp | agent_tool | …" }, + "approvalRequired": { "type": "boolean" }, + "stateful": { "type": "boolean" }, + "timeoutSeconds": { "type": "integer" }, + "maxCalls": { "type": "integer" }, + "config": { "type": "object", "additionalProperties": true }, + "guardrails": { "type": "array", "items": { "$ref": "#/$defs/guardrail" } } + } + }, + "guardrail": { + "type": "object", + "additionalProperties": true, + "description": "Guardrail config. Type-specific keys (patterns, mode, model, policy, …) are spread in, so unknown keys are permitted.", + "properties": { + "name": { "type": "string" }, + "guardrailType": { "type": "string", "description": "regex | llm | custom | external | …" }, + "position": { "type": "string", "enum": ["input", "output"] }, + "onFail": { "type": "string", "enum": ["retry", "raise", "fix", "human"] }, + "maxRetries": { "type": "integer" }, + "taskName": { "type": "string" }, + "patterns": { "type": "array", "items": { "type": "string" } }, + "mode": { "type": "string" }, + "message": { "type": "string" }, + "model": { "type": "string" }, + "policy": { "type": "string" }, + "maxTokens": { "type": "integer" } + } + }, + "termination": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { "type": "string", "enum": ["text_mention", "stop_message", "max_message", "token_usage", "and", "or"] }, + "text": { "type": "string" }, + "caseSensitive": { "type": "boolean" }, + "stopMessage": { "type": "string" }, + "maxMessages": { "type": "integer" }, + "maxTotalTokens": { "type": "integer" }, + "maxPromptTokens": { "type": "integer" }, + "maxCompletionTokens": { "type": "integer" }, + "conditions": { "type": "array", "items": { "$ref": "#/$defs/termination" } } + } + }, + "handoff": { + "type": "object", + "additionalProperties": false, + "required": ["target"], + "properties": { + "type": { "type": "string", "enum": ["on_text_mention", "on_tool_result", "on_condition"] }, + "target": { "type": "string" }, + "toolName": { "type": "string" }, + "resultContains": { "type": "string" }, + "text": { "type": "string" }, + "taskName": { "type": "string" } + } + }, + "callback": { + "type": "object", + "additionalProperties": false, + "required": ["position", "taskName"], + "properties": { + "position": { "type": "string", "enum": ["before_agent", "after_agent", "before_model", "after_model", "before_tool", "after_tool"] }, + "taskName": { "type": "string" } + } + }, + "memory": { + "type": "object", + "additionalProperties": false, + "properties": { + "messages": { "type": "array", "items": { "$ref": "#/$defs/message" } }, + "maxMessages": { "type": "integer" } + } + }, + "message": { + "type": "object", + "additionalProperties": true, + "properties": { + "role": { "type": "string", "enum": ["user", "assistant", "system"] }, + "message": { "type": "string" } + } + }, + "codeExecution": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "allowedLanguages": { "type": "array", "items": { "type": "string" } }, + "allowedCommands": { "type": "array", "items": { "type": "string" } }, + "timeout": { "type": "integer" } + } + }, + "cliConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "allowedCommands": { "type": "array", "items": { "type": "string" } }, + "timeout": { "type": "integer" }, + "allowShell": { "type": "boolean" }, + "workingDir": { "type": "string" } + } + }, + "thinkingConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "budgetTokens": { "type": "integer" } + } + }, + "prefillTool": { + "type": "object", + "additionalProperties": false, + "required": ["toolName"], + "properties": { + "toolName": { "type": "string" }, + "arguments": { "type": "object", "additionalProperties": true } + } + }, + "plannerContextEntry": { + "type": "object", + "additionalProperties": false, + "properties": { + "text": { "type": "string" }, + "url": { "type": "string" }, + "headers": { "type": "object", "additionalProperties": { "type": "string" } }, + "required": { "type": "boolean" }, + "maxBytes": { "type": "integer" } + } + }, + "outputType": { + "type": "object", + "additionalProperties": false, + "properties": { + "schema": { "type": "object", "additionalProperties": true }, + "className": { "type": "string" } + } + }, + "gate": { + "type": "object", + "additionalProperties": false, + "description": "Sequential-pipeline gate: a text matcher, or a worker-task reference.", + "properties": { + "type": { "type": "string", "enum": ["text_contains"] }, + "text": { "type": "string" }, + "caseSensitive": { "type": "boolean" }, + "taskName": { "type": "string" } + } + }, + "workerRef": { + "type": "object", + "additionalProperties": false, + "properties": { + "taskName": { "type": "string" } + } + } + } +} diff --git a/sdk/java/docs/agent-schema.md b/sdk/java/docs/agent-schema.md new file mode 100644 index 000000000..16374a9c2 --- /dev/null +++ b/sdk/java/docs/agent-schema.md @@ -0,0 +1,163 @@ +# Agent JSON Schema + +[`agent-schema.json`](agent-schema.json) is the canonical wire contract for the agent +configuration that every SDK serializes and sends to the server. SDKs POST it under the +`agentConfig` key of the start/compile request; the server deserializes it into its +`AgentConfig` model and compiles it into a Conductor workflow. + +- **Format:** JSON Schema Draft 2020-12. +- **Convention:** camelCase keys; absent = unset (`@JsonInclude(NON_NULL)` server-side). +- **Recursive:** `agents`, `planner`, `fallback`, and `router` nest a full agent config (`$ref: "#"`). +- **Strictness:** `additionalProperties: false` at the root, so the schema is the *complete* + set of recognized top-level keys. + +## What the schema is derived from + +The server's `AgentConfig` is the canonical target — both SDKs serialize *to* it. The schema +is the reconciliation of three sources: + +| Source | File | +|---|---| +| Server model (deserialization target) | `server/conductor-agentspan/src/main/java/dev/agentspan/runtime/model/AgentConfig.java` (+ nested `*Config` models) | +| Java SDK emit | `sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentConfigSerializer.java` | +| Python SDK emit | `sdk/python/src/agentspan/agents/config_serializer.py` | + +## Proof of correctness + +The schema is correct iff it is **sound** (every key it declares is server-recognized or +explicitly tolerated), **complete** (every key either SDK can emit is permitted), and +**type-consistent** (declared types match the server field and both SDK emit types). Each was +verified in three rounds. + +### Round 1 — static inventory + +Every field of the server `AgentConfig` and its nested models (`ToolConfig`, `GuardrailConfig`, +`MemoryConfig`, `TerminationConfig`, `HandoffConfig`, `CallbackConfig`, `CodeExecutionConfig`, +`CliConfig`, `ThinkingConfig`, `PrefillToolCallConfig`, `OutputTypeConfig`, `WorkerRef`) was +inventoried with its JSON key and type, alongside the exact set of keys each SDK serializer +emits. The server uses Spring's default Jackson config — **no `FAIL_ON_UNKNOWN_PROPERTIES`** — +so unknown keys are ignored, never rejected. The schema is intentionally *stricter* than the +server (closed `additionalProperties`) to serve as a precise contract. + +### Round 2 — discrepancy verification against source + +Cross-source differences were confirmed by reading the source directly (not summaries): + +| Item | Finding | Schema decision | +|---|---|---| +| guardrail `onFail` | Closed enum `retry \| raise \| fix \| human` — Java `OnFail` enum, Python `_VALID_ON_FAIL`, and the server field comment **all agree**. | enum on `onFail` | +| `strategy` | Python emits `strategy: null` for a single agent (`config_serializer.py:91`); Java omits it. | `type: ["string","null"]`, enum includes `null` | +| `sessionId` | Java emits it **inside** `agentConfig` (`AgentConfigSerializer.java:259`); the server `AgentConfig` has no such field and reads it from the request wrapper. | permitted optional key | +| `planSource` | Python emits it in `agentConfig` (`config_serializer.py:240`); the Java SDK sends the static plan via the wrapper's `static_plan`. Server `AgentConfig` has `planSource`. | permitted optional `object` | +| `reasoningEffort` | Values `minimal \| low \| medium \| high` (`agent.py:515`). | enum | + +### Round 3 — empirical conformance + +A maximal agent was serialized by **each** SDK and validated against the schema with a Draft +2020-12 validator, then negative mutations were checked, then the result was compiled on a live +server: + +| Check | Result | +|---|---| +| Schema is a well-formed Draft 2020-12 schema | ✅ | +| **Python** maximal agent (21 keys) validates | ✅ | +| **Java** maximal agent (29 keys; exercises `memory`, `gate`, `termination`, `thinkingConfig`, `codeExecution`, `guardrails`, `tools`, `agents`, `handoffs`) validates | ✅ | +| Negative (make-fail): unknown key, bad `reasoningEffort`, bad `strategy`, wrong `maxTurns` type, missing `name`, bad guardrail `onFail` — **all rejected** | ✅ | +| Java-emitted, schema-valid config compiles on the live server (`POST /agent/compile` → HTTP 200) | ✅ | + +The negative checks establish that the schema has *teeth* — it is not vacuously permissive — and +the live compile establishes that a schema-valid document is genuinely accepted by the server. + +## Top-level field correspondence + +| Schema property | Type | Server `AgentConfig` | Java emit | Python emit | +|---|---|---|---|---| +| `name` | string (required) | ✅ | ✅ | ✅ | +| `description` | string | ✅ | — | — (platform-set) | +| `model` | string\|null | ✅ | ✅ | ✅ | +| `external` | boolean | ✅ | ✅ | ✅ | +| `baseUrl` | string | ✅ | ✅ | ✅ | +| `instructions` | string\|object\|null | ✅ | ✅ | ✅ | +| `introduction` | string | ✅ | ✅ | ✅ | +| `tools` | array→`tool` | ✅ | ✅ | ✅ | +| `agents` | array→`#` | ✅ | ✅ | ✅ | +| `strategy` | string\|null (enum) | ✅ | ✅ | ✅ | +| `router` | `#`\|`workerRef` | ✅ | ✅ | ✅ | +| `guardrails` | array→`guardrail` | ✅ | ✅ | ✅ | +| `maxTurns` | integer | ✅ | ✅ | ✅ | +| `maxTokens` | integer | ✅ | ✅ | ✅ | +| `temperature` | number | ✅ | ✅ | ✅ | +| `timeoutSeconds` | integer | ✅ | ✅ | ✅ | +| `reasoningEffort` | string (enum) | ✅ | ✅ | ✅ | +| `contextWindowBudget` | integer | ✅ | ✅ | ✅ | +| `thinkingConfig` | `thinkingConfig` | ✅ | ✅ | ✅ | +| `memory` | `memory` | ✅ | ✅ | ✅ | +| `termination` | `termination` | ✅ | ✅ | ✅ | +| `outputType` | `outputType` | ✅ | ✅ | ✅ | +| `handoffs` | array→`handoff` | ✅ | ✅ | ✅ | +| `allowedTransitions` | object | ✅ | ✅ | ✅ | +| `callbacks` | array→`callback` | ✅ | ✅ | ✅ | +| `gate` | `gate` | ✅ | ✅ | ✅ | +| `stopWhen` | `workerRef` | ✅ | ✅ | ✅ | +| `enablePlanning` | boolean | ✅ | ✅ | ✅ | +| `planner` | `#` | ✅ | ✅ | ✅ | +| `fallback` | `#` | ✅ | ✅ | ✅ | +| `fallbackMaxTurns` | integer | ✅ | ✅ | ✅ | +| `plannerContext` | array→`plannerContextEntry` | ✅ | ✅ | ✅ | +| `planSource` | object | ✅ | — (via wrapper) | ✅ | +| `synthesize` | boolean | ✅ | ✅ | ✅ | +| `stateful` | boolean | — (domain isolation) | ✅ | ✅ | +| `sessionId` | string | — (read from wrapper) | ✅ | — (sent in wrapper) | +| `includeContents` | string | ✅ | ✅ | ✅ | +| `requiredTools` | array | ✅ | ✅ | ✅ | +| `prefillTools` | array→`prefillTool` | ✅ | ✅ | ✅ | +| `credentials` | array | ✅ | ✅ | ✅ | +| `metadata` | object | ✅ | ✅ | ✅ | +| `localCodeExecution` | boolean | — (→ `codeExecution`) | builder flag | builder flag | +| `codeExecution` | `codeExecution` | ✅ | ✅ | ✅ | +| `cliConfig` | `cliConfig` | ✅ | ✅ | ✅ | +| `maskedFields` | array | ✅ | ✅ | ✅ | + +`—` in the server column marks the two keys the server does not model on `AgentConfig` +(`stateful` drives runtime domain isolation; `sessionId` is read from the request wrapper). Both +are permitted by the schema so that Java's output validates. + +## Known cross-SDK divergences + +These are *correct per the schema* (both forms validate) but worth noting: + +- **Static plan channel.** Python places the static plan in `agentConfig.planSource`; the Java SDK + sends it in the request wrapper as `static_plan`. The server accepts both. +- **Session id channel.** Java echoes `sessionId` into `agentConfig` in addition to the wrapper; + Python only sends it in the wrapper. The server reads it from the wrapper. +- **Tool retry fields.** The Java serializer may emit `retryCount` / `retryDelaySeconds` / + `retryPolicy` on a tool. These are not in the server `ToolConfig` model, so the `tool` definition + keeps `additionalProperties: true`. +- **`cliConfig.workingDir`.** The Java serializer emits `workingDir` inside `cliConfig`; the server + `CliConfig` model has no such field (it is ignored server-side). The schema includes it so Java + output validates. + +## Reverse verification + +As an independent check that the schema is *complete* (the forward pass started from inventories, +which could omit a field), the schema is reverse-engineered back into models and diffed against the +live source by [`generated/generate.py`](generated/generate.py). It emits a Python dataclass +([`generated/agent_config.py`](generated/agent_config.py)) and a Java record +([`generated/AgentConfigModel.java`](generated/AgentConfigModel.java)) **directly from the schema**, +then asserts: + +| Check | Result | +|---|---| +| (a) generated dataclass/record fields are field-for-field identical to the schema (root + 16 nested) | ✅ | +| (b) a generated instance serializes to schema-valid JSON | ✅ | +| (c) every server `AgentConfig` field — root **and** all 13 nested models (`ToolConfig`, `GuardrailConfig`, `TerminationConfig`, …) — is present in the schema | ✅ **0 gaps** | + +The generated Java record also compiles under `javac`. The only properties the schema carries +beyond the server models are the documented SDK-emitted/tolerated extras: `sessionId`, `stateful`, +`localCodeExecution` (root) and `cliConfig.workingDir` — nothing was missed. + +## Scope + +The schema describes **native** agent configs. Framework-bridged agents (`openai`, `google_adk`, +`skill`, …) take a different serialization path and are sent as an opaque `rawConfig` under a +`framework` key in the request wrapper; they are out of scope for this schema. diff --git a/sdk/java/docs/agent-structure.md b/sdk/java/docs/agent-structure.md new file mode 100644 index 000000000..4173c67a7 --- /dev/null +++ b/sdk/java/docs/agent-structure.md @@ -0,0 +1,88 @@ +# Agent — Field Reference + +`Agent` is the declarative configuration you build with `Agent.builder()`. Each field +below lists its builder method, the JSON key it serializes to, and any behavior notes. + +## Fields + +| Field | Builder method | JSON key | Notes | +|---|---|---|---| +| `name` | `name(String)` | `name` | Required. Pattern `^[a-zA-Z_][a-zA-Z0-9_-]*$`. | +| `model` | `model(String)` | `model` | `"provider/model"` format. Omitted for external agents. | +| `instructions` | `instructions(String)` | `instructions` | System prompt. Overridden by `instructionsTemplate` if set. | +| `instructionsTemplate` | `instructionsTemplate(PromptTemplate)` | `instructions` (structured) | Emitted as a `PromptTemplateRef` map. Takes precedence over plain instructions. | +| `introduction` | `introduction(String)` | `introduction` | Prepended before the first user message in multi-agent discussions. | +| `tools` | `tools(ToolDef...)` | `tools` | List of tool configs. | +| `agents` | `agents(Agent...)` | `agents` | Sub-agents (recursive). Requires a strategy. | +| `strategy` | `strategy(Strategy)` | `strategy` | Emitted only when sub-agents or planner/fallback slots are present. Default `HANDOFF`. | +| `router` | `router(Agent)` | `router` | For `ROUTER` strategy. Nested agent config. | +| `guardrails` | `guardrails(GuardrailDef...)` | `guardrails` | Input/output guardrail configs. | +| `maxTurns` | `maxTurns(int)` | `maxTurns` | Default 25. Emitted only if > 0. | +| `maxTokens` | `maxTokens(int)` | `maxTokens` | LLM token cap. | +| `temperature` | `temperature(double)` | `temperature` | Sampling temperature. | +| `timeoutSeconds` | `timeoutSeconds(int)` | `timeoutSeconds` | Always emitted (including `0`). `0` → server applies its default. | +| `termination` | `termination(TerminationCondition)` | `termination` | e.g. `MaxMessageTermination`, `StopMessageTermination`. | +| `outputType` | `outputType(Class)` | `outputType` | Structured-output class name. | +| `handoffs` | `handoffs(Handoff...)` | `handoffs` | SWARM triggers: `OnTextMention`, `OnToolResult`, `OnCondition`. | +| `allowedTransitions` | `allowedTransitions(Map)` | `allowedTransitions` | SWARM: restricts which agents may transfer to which. | +| `credentials` | `credentials(String...)` | `credentials` | Secret names fetched from the secrets store at runtime. | +| `requiredTools` | `requiredTools(String...)` | `requiredTools` | Tool names that must be called during the run. | +| `metadata` | `metadata(Map)` | `metadata` | Arbitrary key-values stored with the workflow definition. | +| `synthesize` | `synthesize(boolean)` | `synthesize` | Emitted only when `false` (default `true`). | +| `stateful` | `stateful(boolean)` | `stateful` | Emitted as `true` when set. Triggers per-execution domain isolation (`runId`). | +| `sessionId` | `sessionId(String)` | `sessionId` | Emitted inside `agentConfig` and as a top-level `AgentRequest` field. | +| `baseUrl` | `baseUrl(String)` | `baseUrl` | Per-agent LLM provider endpoint override. | +| `includeContents` | `includeContents(String)` | `includeContents` | `"none"` = fresh context; absent = inherit parent context. | +| `thinkingBudgetTokens` | `thinkingBudgetTokens(int)` | `thinkingConfig` | Emitted as `{enabled: true, budgetTokens: N}`. Anthropic extended thinking. | +| `enablePlanning` | `enablePlanning(boolean)` | `enablePlanning` | Prepends a "plan first" preamble to the system prompt. | +| `prefillTools` | `prefillTools(List)` | `prefillTools` | Tool calls executed before the first LLM turn; results injected as context. | +| `planner` | `planner(Agent)` | `planner` | `PLAN_EXECUTE` named slot. | +| `fallback` | `fallback(Agent)` | `fallback` | `PLAN_EXECUTE` fallback agent. | +| `fallbackMaxTurns` | `fallbackMaxTurns(int)` | `fallbackMaxTurns` | `PLAN_EXECUTE` only. | +| `plannerContext` | `plannerContext(List)` | `plannerContext` | `PLAN_EXECUTE` only. Text/URL context appended to the planner's prompt. Throws at `build()` if strategy ≠ `PLAN_EXECUTE`. | +| `localCodeExecution` | `localCodeExecution(boolean)` | `codeExecution` | Emitted as a `CodeExecutionConfig`. Injects a `run_code` worker tool. | +| `allowedLanguages` | `allowedLanguages(List)` | `codeExecution.allowedLanguages` | Default `["python"]`. | +| `codeExecutionTimeout` | `codeExecutionTimeout(int)` | `codeExecution.timeout` | Default 30s. | +| `allowedCommands` | `allowedCommands(List)` | `codeExecution.allowedCommands` | Shell commands permitted during code execution. | +| `cliConfig` | `cliConfig(CliConfig)` | `cliConfig` | Injects a `run_command` worker tool. | +| `gate` | `gate(TextGate)` | `gate` | Sequential pipeline gate: `{type: "text_contains", text, caseSensitive}`. | +| `stopWhenTaskName` | `stopWhen(String)` | `stopWhen` | Emitted as `{taskName: name}`. | +| `callbacks` | `callbacks(CallbackHandler...)` | `callbacks` | Introspected for `before_model`, `after_model`, `before_agent`, `after_agent`. | +| `beforeModelCallback` | `beforeModelCallback(Function)` | `callbacks` (position `before_model`) | Emitted as a callback entry. | +| `afterModelCallback` | `afterModelCallback(Function)` | `callbacks` (position `after_model`) | Emitted as a callback entry. | +| `beforeAgentCallback` | `beforeAgentCallback(Function)` | `callbacks` (position `before_agent`) | Emitted as a callback entry. | +| `afterAgentCallback` | `afterAgentCallback(Function)` | `callbacks` (position `after_agent`) | Emitted as a callback entry. | +| `memory` | `memory(ConversationMemory)` | `memory` | Emitted as `{messages, maxMessages}`. Multi-turn message history. | +| `reasoningEffort` | `reasoningEffort(String)` | `reasoningEffort` | OpenAI reasoning models: `"low"`, `"medium"`, `"high"`. Ignored by other models. | +| `maskedFields` | `maskedFields(String...)` | `maskedFields` | Field names redacted in execution history/UI. | +| `contextWindowBudget` | `contextWindowBudget(int)` | `contextWindowBudget` | Token threshold for proactive context condensation. | +| `framework` | `framework(String)` | _(dispatch key)_ | Selects the serialization path; not emitted as a field. | +| `frameworkConfig` | `frameworkConfig(Map)` | _(merged at top level)_ | For framework agents; entries merged into the output map. | + +## Serialization behavior + +A few fields serialize non-obviously: + +- **Strategy** is emitted only when sub-agents or `PLAN_EXECUTE` named slots (`planner`/`fallback`) + are present, so a plain single agent does not carry a redundant `"strategy": "handoff"`. +- **Framework dispatch** — `framework` is a dispatch key, not a field. Native agents serialize + as `{agentConfig}`; framework-backed agents (`"skill"`, `"openai"`, `"google_adk"`) take an + early-exit path and serialize as `{framework, rawConfig}`, with `frameworkConfig` merged into + the top level of the output. +- **synthesize** defaults to `true` and is emitted only when `false`. +- **Callbacks** — the four function-typed callbacks and the `callbacks` list all serialize into a + single `callbacks` list. Function objects are never sent; each becomes a Conductor task + reference at its position, and the runtime registers the function as a local worker. + +## Defaults + +| Field | Builder default | +|---|---| +| `strategy` | `HANDOFF` | +| `maxTurns` | `25` | +| `timeoutSeconds` | `0` (→ server default) | +| `codeExecutionTimeout` | `30` seconds | +| `synthesize` | `true` | +| `stateful` | `false` | +| `enablePlanning` | `false` | +| `localCodeExecution` | `false` | diff --git a/sdk/java/docs/api-reference.md b/sdk/java/docs/api-reference.md new file mode 100644 index 000000000..2b1b00bb4 --- /dev/null +++ b/sdk/java/docs/api-reference.md @@ -0,0 +1,406 @@ +# API Reference + +Complete method signatures for the Agentspan Java SDK public API. + +## AgentRuntime + +The SDK entry point. Thread-safe — share one instance. + +```java +// Constructors +AgentRuntime() // reads AGENTSPAN_* env vars +AgentRuntime(AgentConfig config) // env vars + explicit tuning +AgentRuntime(ApiClient client) // explicit client +AgentRuntime(ApiClient client, AgentConfig config) + +// Static factories for ApiClient +static ApiClient clientFromEnv() +static ApiClient client(String serverUrl) +static ApiClient client(String serverUrl, String authKey, String authSecret) +``` + +### Run + +```java +AgentResult run(Agent agent, String prompt) +AgentResult run(Agent agent, String prompt, Plan plan) + +CompletableFuture runAsync(Agent agent, String prompt) +CompletableFuture runAsync(Agent agent, String prompt, Plan plan) +``` + +### Start (fire-and-forget) + +```java +AgentHandle start(Agent agent, String prompt) +CompletableFuture startAsync(Agent agent, String prompt) +CompletableFuture startAsync(Agent agent, String prompt, Plan plan) +``` + +### Stream + +```java +AgentStream stream(Agent agent, String prompt) +CompletableFuture streamAsync(Agent agent, String prompt) +``` + +### Deploy / serve + +```java +CompileResponse plan(Agent agent) // compile only, no run +List deploy(Agent... agents) +DeploymentInfo deploy(Agent agent, List schedules) +CompletableFuture> deployAsync(Agent... agents) +void serve(Agent... agents) // blocks indefinitely +``` + +### Resume / schedule + +```java +AgentHandle resume(String executionId, Agent agent) +CompletableFuture resumeAsync(String executionId, Agent agent) +Schedules schedules() +``` + +### Lifecycle + +```java +void shutdown() // stop workers, release HTTP connections +void close() // alias for shutdown(); implements AutoCloseable +``` + +--- + +## Agent.Builder + +```java +Agent.builder() + // Identity + .name(String) // required + .model(String) // required + .instructions(String) + .instructions(Supplier) // dynamic — re-evaluated on each run submission + .instructionsTemplate(PromptTemplate) + .introduction(String) + .metadata(Map) + + // LLM + .maxTurns(int) // default 25 + .maxTokens(int) + .temperature(double) + .thinkingBudgetTokens(int) // Anthropic extended thinking + .reasoningEffort(String) // OpenAI reasoning models: "low"|"medium"|"high" + .contextWindowBudget(int) // token threshold for proactive condensation + .timeoutSeconds(int) // default 0 (server applies its own default) + + // Tools + .tools(List) + .tools(ToolDef...) + + // Multi-agent + .agents(List) + .agents(Agent...) + .strategy(Strategy) + .router(Agent) + .handoffs(List) + .handoffs(Handoff...) + .allowedTransitions(Map>) + + // Termination + .termination(TerminationCondition) + + // Guardrails + .guardrails(List) + .guardrails(GuardrailDef...) + + // Auth + .credentials(List) + .credentials(String...) + + // Code execution + .localCodeExecution(boolean) + .allowedLanguages(List) + .codeExecutionTimeout(int) // seconds + + // Callbacks (intercept the agent loop) + .beforeModelCallback(Function, Map>) + .afterModelCallback(Function, Map>) + .beforeAgentCallback(Function, Map>) + .afterAgentCallback(Function, Map>) + .callbacks(List) + .callbacks(CallbackHandler...) + + // Stateful (session isolation) + .sessionId(String) + .stateful(boolean) + .memory(ConversationMemory) // multi-turn message history + + // Privacy + .maskedFields(String...) // redact fields in history/UI + + // Advanced + .outputType(Class) // structured output + .fallback(Agent) + .fallbackMaxTurns(int) + .planner(Agent) + .plannerContext(List) + .plannerContext(String...) + .prefillTools(List) + .synthesize(boolean) + .enablePlanning(boolean) + .baseUrl(String) + .gate(TextGate) + .includeContents(String) + .cliConfig(CliConfig) + .requiredTools(String...) + .stopWhen(String) // task name to stop on + .allowedCommands(List) + .framework(String) // for bridge agents + .frameworkConfig(Map) + + .build() +``` + +--- + +## @AgentDef annotation + +Declarative alternative to the builder — annotate a method to define an agent +(see [Agents](concepts/agents.md#agentdef-annotation) for attribute details): + +```java +import org.conductoross.conductor.ai.annotations.AgentDef; + +public class Weather { + @Tool(name = "get_weather", description = "Get weather for a city") + public String getWeather(String city) { return "Sunny, 72F in " + city; } + + @AgentDef(model = "openai/gpt-4o") // @Tool methods attach automatically + public String weatherbot() { + // returned String = instructions; no-arg form is lazy — re-evaluated per run + return "You are a weather assistant. Today is " + LocalDate.now() + "."; + } + + @AgentDef(model = "openai/gpt-4o") // optional Agent.Builder param = full builder API + public void researcher(Agent.Builder builder) { + builder.termination(new MaxMessageTermination(10)); + } + + @AgentDef // return Agent (or Agent.Builder) = full factory + public Agent reviewer() { + return Agent.builder().name("reviewer").model("openai/gpt-4o") + .instructions("Review the draft.").build(); + } + + @AgentDef(model = "openai/gpt-4o") // return PromptTemplate = server-side template + public PromptTemplate support() { + return new PromptTemplate("customer-support", Map.of("tone", "friendly")); + } +} +``` + +```java +List agents = Agent.fromInstance(instance); // resolve all @AgentDef methods +Agent agent = Agent.fromInstance(instance, "name"); // resolve one by name +``` + +--- + +## AgentResult + +```java +Object getOutput() // final LLM output (String or structured object) + T getOutput(Class type) // deserialize structured output +AgentStatus getStatus() // COMPLETED | FAILED | TERMINATED | TIMED_OUT +String getExecutionId() +List> getToolCalls() +List getEvents() +TokenUsage getTokenUsage() +boolean isSuccess() +String getError() +``` + +--- + +## AgentHandle + +```java +String getExecutionId() +AgentResult waitForResult() // blocks; default 600s timeout +AgentResult waitForResult(long timeoutMs, long pollMs) // explicit timeout +boolean waitUntilWaiting(long timeoutMs) // wait for HITL pause +boolean isWaiting() // true if a HITL task is paused +void approve() +void approve(String comment) +void reject(String reason) +void respond(Map data) // arbitrary HITL response (MANUAL strategy) +``` + +--- + +## AgentStream + +`AgentStream` implements `Iterable` and `AutoCloseable`. + +```java +try (AgentStream stream = runtime.stream(agent, prompt)) { + for (AgentEvent event : stream) { + EventType type = event.getType(); // MESSAGE | TOOL_CALL | TOOL_RESULT | … + String content = event.getContent(); + String toolName = event.getToolName(); + Map args = event.getArgs(); + String executionId = event.getExecutionId(); + } +} + +// Approve from a stream +stream.approve(event); +stream.reject(event, "reason"); +``` + +--- + +## Tool builders + +```java +// @Tool-annotated POJO → list of worker tools +ToolRegistry.fromInstance(Object pojo) // returns List + +// Sub-agent as a tool +AgentTool.from(Agent agent) +AgentTool.from(Agent agent, String description) + +// HTTP +HttpTool.builder().name(String).description(String).url(String).method(String).build() + +// MCP +McpTool.builder().name(String).description(String).serverUrl(String).build() + +// Human +HumanTool.create(String name, String description) + +// PDF +PdfTool.create(String name, String description) + +// Wait for message +WaitForMessageTool.create(String name, String description) + +// Image / media +MediaTools.imageTool(String name, String description, String provider, String model) +``` + +--- + +## Termination conditions + +```java +MaxMessageTermination.of(int maxMessages) +StopMessageTermination.of(String stopMessage) +TextMentionTermination.of(String text) +TextMentionTermination.of(String text, boolean caseSensitive) +TokenUsageTermination.ofTotal(int maxTokens) + +// Compose +condition.and(TerminationCondition other) // both must be true +condition.or(TerminationCondition other) // either must be true +``` + +--- + +## Handoffs + +```java +OnTextMention.of(String text, String targetAgent) +OnToolResult.of(String toolName, String targetAgent) +OnToolResult.of(String toolName, String targetAgent, String resultContains) +new OnCondition(String targetAgent, Function,Boolean> predicate) +``` + +--- + +## Schedules + +```java +Schedules schedules = runtime.schedules(); + +schedules.save(Schedule schedule, String agentName) +schedules.get(String wireName) // → ScheduleInfo +schedules.list(String agentName) // → List +schedules.runNow(ScheduleInfo info) // → String executionId +schedules.pause(String wireName) +schedules.pause(String wireName, String reason) +schedules.resume(String wireName) +schedules.delete(String wireName) +schedules.previewNext(String cron, int n) // → List (epoch ms) + +// Schedule.builder() +Schedule.builder() + .name(String) // required + .cron(String) // required; standard 5-field cron + .timezone(String) // default "UTC" + .input(Map) + .description(String) + .paused(boolean) + .catchup(boolean) + .startAt(long) // epoch ms + .endAt(long) // epoch ms + .build() +``` + +--- + +## Credentials + +```java +// In a @Tool method (ToolContext required as last param): +String value = Credentials.get("SECRET_NAME"); +String value = Credentials.getOrNull("SECRET_NAME"); // null if not found + +// Store via CLI: +// agentspan secrets set SECRET_NAME value +``` + +--- + +## Skill + +```java +Agent Skill.skill(Path path, String model) +Agent Skill.skill(Path path, String model, Map agentModels) +Map Skill.loadSkills(Path directory, String model) +``` + +--- + +## Framework bridges + +```java +// LangChain4j +Agent LangChain4jAgent.from(String name, String model, String instructions, Object... tools) +boolean LangChain4jAgent.isLangChain4jTools(Object obj) + +// OpenAI Agents SDK style +OpenAIAgent.builder() + .name(String).model(String).instructions(String) + .tools(Object...) // @Tool-annotated POJOs + .handoffs(Agent...) + .outputType(String) + .build() + +// Google ADK +Agent AdkBridge.toAgentspan(BaseAgent adkAgent) +Agent.Builder AdkBridge.agentBuilder(BaseAgent adkAgent) +``` + +--- + +## AgentConfig + +```java +new AgentConfig() // defaults: 100ms poll, 1 thread +new AgentConfig(int pollIntervalMs, int threads) +AgentConfig.fromEnv() // reads AGENTSPAN_WORKER_* env vars + +config.getWorkerPollIntervalMs() +config.getWorkerThreadCount() +``` diff --git a/sdk/java/docs/concepts/agents.md b/sdk/java/docs/concepts/agents.md new file mode 100644 index 000000000..04329152c --- /dev/null +++ b/sdk/java/docs/concepts/agents.md @@ -0,0 +1,302 @@ +# Agents + +`Agent` is the single orchestration primitive. One agent wraps an LLM plus tools. An agent whose tools include other agents is a multi-agent system — no separate Team or Swarm classes. + +## Builder + +```java +Agent agent = Agent.builder() + .name("my_agent") // required; becomes the Conductor workflow name + .model("openai/gpt-4o-mini") // required; "provider/model" format + .instructions("You are a helpful agent.") // system prompt + .build(); +``` + +Every field below is optional. + +### @AgentDef annotation + +Instead of the builder, a method can be annotated with `@AgentDef` (the Java counterpart of the Python SDK's `@agent` decorator). The method body returns the instructions; `@Tool` and `@GuardrailDef` methods on the same object are attached automatically. + +```java +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.annotations.AgentDef; +import org.conductoross.conductor.ai.annotations.Tool; + +public class Weather { + @Tool(name = "get_weather", description = "Get weather for a city") + public String getWeather(String city) { return "Sunny, 72F in " + city; } + + @AgentDef(model = "openai/gpt-4o") + public String weatherbot() { + return "You are a weather assistant."; + } +} + +Agent agent = Agent.fromInstance(new Weather(), "weatherbot"); +``` + +| Attribute | Default | Description | +|---|---|---| +| `name` | method name | Agent name. | +| `model` | `""` | `"provider/model"`. When empty and used as a sub-agent, inherits the parent's model. | +| `instructions` | `""` | Static system prompt. A non-empty `String` returned by the method wins over this attribute. | +| `tools` | `{"*"}` | Names of `@Tool` methods on the same object. `{"*"}` = all, `{}` = none. | +| `guardrails` | `{"*"}` | Names of `@GuardrailDef` methods on the same object. Same wildcard rules. | +| `agents` | `{}` | Names of other `@AgentDef` methods on the same object, used as sub-agents. | +| `strategy` | `HANDOFF` | Multi-agent strategy. | +| `maxTurns` | `25` | Maximum agent loop iterations. | +| `maxTokens` | unset | LLM `max_tokens` (`0` = unset). | +| `temperature` | unset | Sampling temperature (`NaN` = unset). | +| `credentials` | `{}` | Agent-level credential names. | +| `contextWindowBudget` | unset | Proactive condensation threshold (`0` = unset). | + +**Method contract.** The return type declares what the method provides: + +| Return type | Meaning | +|---|---| +| `void` | Nothing — the annotation attributes alone define the agent. | +| `String` | Dynamic instructions. A no-arg method is **lazy**: re-invoked on every run submission (when the config is serialized), so the prompt can reflect current state. A non-empty result wins over the `instructions` attribute. | +| `PromptTemplate` | Server-side instructions template (`instructionsTemplate`); invoked once. | +| `Agent.Builder` | The definition itself — the returned builder is built. | +| `Agent` | The definition itself, returned as-is (full factory, CrewAI-style). | + +The method may take no parameters, or a single `Agent.Builder` parameter — the escape hatch to the full builder API. The builder arrives pre-populated from the annotation and the discovered tools/guardrails/sub-agents; the method body can then apply anything the builder supports, including sub-agents defined in other classes. Builder-param methods are invoked exactly once (a customizer must not be replayed per run): + +```java +public class Research { + @AgentDef(model = "openai/gpt-4o", instructions = "You are a researcher.") + public void researcher(Agent.Builder builder) { + builder.termination(new MaxMessageTermination(10)) + .agents(Agent.fromInstance(new Editing(), "editor")); + } + + // full factory — annotation is a discovery marker; attributes other than name are rejected + @AgentDef + public Agent reviewer() { + return Agent.builder().name("reviewer").model("openai/gpt-4o") + .instructions("Review the draft.").build(); + } +} +``` + +`Agent.fromInstance(obj)` resolves all `@AgentDef` methods on an object; `Agent.fromInstance(obj, "name")` resolves one. For factory methods the lookup name is still the annotation `name`/method name, while the agent keeps the name the factory set. + +Dynamic instructions are also available directly on the builder, without the annotation: `Agent.builder().instructions(() -> "Today is " + LocalDate.now())` — the supplier is re-evaluated on each run submission, matching the Python SDK's callable instructions. + +**Discovery rules.** `@AgentDef` methods must be `public` (a non-public annotated method throws rather than being silently ignored) and cannot also carry `@Tool` or `@GuardrailDef`. Discovery walks the full type hierarchy — superclasses and interfaces (including `default` methods) — and the nearest annotated declaration wins. An unannotated override does *not* hide the agent: the ancestor's annotation is used and invocation dispatches to the override, so CGLIB-proxied Spring beans (`@Transactional` etc.) keep working. In Spring Boot apps, the auto-configured [`AgentCatalog`](../spring-boot.md) collects `@AgentDef` agents from every bean. + +### Identity + +| Builder method | Type | Default | Description | +|---|---|---|---| +| `name(String)` | `String` | — | **Required.** Unique workflow name. Use `snake_case`. | +| `model(String)` | `String` | — | **Required.** `"provider/model"`, e.g. `"openai/gpt-4o"`, `"anthropic/claude-opus-4-8"`. | +| `instructions(String)` | `String` | `""` | System prompt. | +| `instructionsTemplate(PromptTemplate)` | `PromptTemplate` | `null` | Prompt stored on the server; use `PromptTemplate.of("name")`. | +| `introduction(String)` | `String` | `null` | Injected before the first user message (not the system prompt). | +| `metadata(Map)` | `Map` | `{}` | Arbitrary key-value metadata stored with the workflow. | + +### LLM tuning + +| Builder method | Type | Default | Description | +|---|---|---|---| +| `maxTurns(int)` | `int` | `25` | Maximum agent loop iterations before termination. | +| `maxTokens(int)` | `int` | `null` | LLM `max_tokens`. | +| `temperature(double)` | `double` | `null` | LLM sampling temperature. | +| `thinkingBudgetTokens(int)` | `int` | `null` | Extended thinking budget (Anthropic only). | +| `timeoutSeconds(int)` | `int` | `0` | Overall agent execution timeout. `0` lets the server apply its own default. | + +### Tools + +```java +import org.conductoross.conductor.ai.internal.ToolRegistry; + +Agent agent = Agent.builder() + .name("tool_agent") + .model("openai/gpt-4o-mini") + .tools(ToolRegistry.fromInstance(new MyTools())) // from @Tool-annotated POJO + .tools(HttpTool.builder() + .name("search") + .url("https://api.example.com/search") + .method("GET").build()) // HTTP tool + .build(); +``` + +See [Tools](tools.md) for all tool types. + +### Multi-agent + +```java +Agent pipeline = writer.then(editor); // .then() = Strategy.SEQUENTIAL shorthand + +Agent team = Agent.builder() + .name("team") + .model("openai/gpt-4o-mini") + .agents(writer, editor, reviewer) + .strategy(Strategy.PARALLEL) + .build(); +``` + +See [Multi-Agent](multi-agent.md) for all strategies. + +### Guardrails + +```java +import org.conductoross.conductor.ai.guardrail.RegexGuardrail; +import org.conductoross.conductor.ai.guardrail.LLMGuardrail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.enums.OnFail; + +Agent agent = Agent.builder() + .name("safe_agent") + .model("openai/gpt-4o-mini") + .guardrails( + RegexGuardrail.builder() + .name("no_phone").position(Position.OUTPUT) + .patterns("\\d{10}").onFail(OnFail.RAISE).build(), + LLMGuardrail.builder() + .name("tone_check").position(Position.OUTPUT) + .model("openai/gpt-4o-mini").policy("The tone must be professional.") + .onFail(OnFail.RETRY).build()) + .build(); +``` + +See [Guardrails](guardrails.md). + +### Credentials + +Declare which secrets the agent's tools require. The SDK fetches them from the Agentspan secrets store at runtime and injects them into tool context. + +```java +Agent agent = Agent.builder() + .name("github_agent") + .model("openai/gpt-4o-mini") + .credentials("GITHUB_TOKEN", "JIRA_API_KEY") + .build(); + +// In the tool: +public String createIssue(String title, ToolContext ctx) { + String token = Credentials.get("GITHUB_TOKEN"); + // ... +} +``` + +### Code execution + +```java +Agent agent = Agent.builder() + .name("coder") + .model("openai/gpt-4o-mini") + .localCodeExecution(true) + .allowedLanguages(List.of("python", "javascript")) + .codeExecutionTimeout(30) // seconds per execution + .build(); +``` + +### Callbacks + +Intercept the agent loop before or after each model call: + +```java +Agent agent = Agent.builder() + .name("observed_agent") + .model("openai/gpt-4o-mini") + .beforeModelCallback(ctx -> { + System.out.println("Calling LLM with: " + ctx.get("messages")); + return ctx; // return modified context or the original + }) + .afterModelCallback(ctx -> { + System.out.println("LLM replied: " + ctx.get("output")); + return ctx; + }) + .build(); +``` + +### Fallback + +Run a second agent if the first exceeds `fallbackMaxTurns`: + +```java +Agent agent = Agent.builder() + .name("primary") + .model("openai/gpt-4o-mini") + .fallback(Agent.builder().name("backup").model("anthropic/claude-haiku-4-5-20251001").build()) + .fallbackMaxTurns(5) + .build(); +``` + +--- + +## Running an agent + +### AgentRuntime + +`AgentRuntime` is the SDK entry point. Use try-with-resources — `close()` stops worker threads and releases HTTP connections. + +```java +try (AgentRuntime runtime = new AgentRuntime()) { + AgentResult result = runtime.run(agent, "Hello!"); +} +``` + +| Method | Returns | Description | +|---|---|---| +| `run(agent, prompt)` | `AgentResult` | Blocking — waits for completion. | +| `run(agent, prompt, plan)` | `AgentResult` | Blocking with a deterministic plan. | +| `runAsync(agent, prompt)` | `CompletableFuture` | Non-blocking. | +| `start(agent, prompt)` | `AgentHandle` | Fire-and-forget; returns a handle to poll or approve. | +| `startAsync(agent, prompt)` | `CompletableFuture` | Non-blocking start. | +| `stream(agent, prompt)` | `AgentStream` | Blocking iterator over events. | +| `streamAsync(agent, prompt)` | `CompletableFuture` | Non-blocking stream. | +| `plan(agent)` | `CompileResponse` | Compile only — returns `getWorkflowDef()` + `getRequiredWorkers()` without executing. | +| `deploy(Agent...)` | `List` | Register workflow definitions without running them. | +| `serve(Agent...)` | `void` | Long-running worker mode — keeps polling indefinitely. | +| `resume(executionId, agent)` | `AgentHandle` | Resume a suspended execution. | +| `schedules()` | `Schedules` | Access the scheduling API. | + +### AgentResult + +```java +AgentResult result = runtime.run(agent, "prompt"); + +result.getOutput(); // Object — final LLM output (String or structured object) +result.getStatus(); // AgentStatus.COMPLETED / FAILED / TERMINATED / TIMED_OUT +result.getExecutionId(); // String — Conductor workflow ID +result.getToolCalls(); // List> — all tool invocations +result.getEvents(); // List — full event log +result.getTokenUsage(); // TokenUsage — prompt/completion/total tokens +result.isSuccess(); // true if status == COMPLETED +result.getError(); // String — error message if failed +``` + +### AgentHandle + +Returned by `start()` — lets you poll, approve, or stream after the fact: + +```java +AgentHandle handle = runtime.start(agent, "prompt"); +String executionId = handle.getExecutionId(); + +// Poll until done (blocks the calling thread) +AgentResult result = handle.waitForResult(); + +// Or approve a human-in-the-loop step +handle.approve("Looks good"); +handle.reject("Not acceptable"); +``` + +### Concurrency + +`AgentRuntime` is thread-safe. Share one instance across threads rather than creating one per request. + +```java +// Good — one shared runtime +private static final AgentRuntime runtime = new AgentRuntime(); + +// Run multiple agents concurrently +List> futures = prompts.stream() + .map(p -> runtime.runAsync(agent, p)) + .toList(); +CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); +``` diff --git a/sdk/java/docs/concepts/guardrails.md b/sdk/java/docs/concepts/guardrails.md new file mode 100644 index 000000000..160ec1708 --- /dev/null +++ b/sdk/java/docs/concepts/guardrails.md @@ -0,0 +1,131 @@ +# Guardrails + +Guardrails validate or modify agent input and output. They run before the agent sees a message (`INPUT`) or after the agent produces a response (`OUTPUT`). + +There are three kinds, each with its own builder — all produce a `GuardrailDef`: + +- `RegexGuardrail.builder()` — pattern matching (`guardrailType="regex"`) +- `LLMGuardrail.builder()` — LLM-judged policy (`guardrailType="llm"`) +- `GuardrailDef.builder().func(...)` — a custom Java function (`guardrailType="custom"`) + +## Quick example + +```java +import org.conductoross.conductor.ai.guardrail.RegexGuardrail; +import org.conductoross.conductor.ai.guardrail.LLMGuardrail; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; + +Agent agent = Agent.builder() + .name("safe_agent") + .model("openai/gpt-4o-mini") + .guardrails( + // Block output containing a phone-number pattern + RegexGuardrail.builder() + .name("no_phone_numbers") + .position(Position.OUTPUT) + .patterns("\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b") + .onFail(OnFail.RAISE) + .build(), + + // Ask an LLM to enforce a policy; retry the turn if it fails + LLMGuardrail.builder() + .name("professional_tone") + .position(Position.OUTPUT) + .model("openai/gpt-4o-mini") + .policy("The response must be professional and free of slang.") + .onFail(OnFail.RETRY) + .build()) + .build(); +``` + +## Guardrail types + +### Regex — `RegexGuardrail` + +Match one or more patterns against the content. + +```java +RegexGuardrail.builder() + .name("no_secrets") + .position(Position.OUTPUT) + .patterns("password", "secret", "api[_-]?key") // varargs or List + .message("Output blocked: contained a secret") // optional + .onFail(OnFail.RAISE) + .build(); +``` + +### LLM — `LLMGuardrail` + +Ask a language model to evaluate the content against a policy. + +```java +LLMGuardrail.builder() + .name("safe_content") + .position(Position.OUTPUT) + .model("openai/gpt-4o-mini") + .policy("Reject any harmful, offensive, or unsafe content.") + .onFail(OnFail.RAISE) + .build(); +``` + +### Custom — `GuardrailDef.builder().func(...)` + +Provide a `Function` for full control. Runs as a local Conductor worker. + +```java +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.GuardrailResult; + +GuardrailDef.builder() + .name("length_check") + .position(Position.OUTPUT) + .func(content -> { + if (content.length() > 5000) { + return GuardrailResult.fail("Response too long: " + content.length() + " chars"); + } + return GuardrailResult.pass(); + }) + .onFail(OnFail.RAISE) + .build(); +``` + +## Common builder options + +| Method | Available on | Default | Description | +|---|---|---|---| +| `name(String)` | all | **required** | Guardrail ID. | +| `position(Position)` | all | `OUTPUT` | `INPUT` or `OUTPUT`. | +| `onFail(OnFail)` | all | `RAISE` | Action when the guardrail fails. | +| `maxRetries(int)` | all | `3` | Retry budget when `onFail == RETRY`. | +| `patterns(String...)` / `patterns(List)` | `RegexGuardrail` | — | Regex patterns to match. | +| `message(String)` | `RegexGuardrail` | — | Custom failure message. | +| `model(String)` | `LLMGuardrail` | — | Judge model, `"provider/model"`. | +| `policy(String)` | `LLMGuardrail` | — | Policy the content must satisfy. | +| `func(Function)` | `GuardrailDef` | — | The check, for custom guardrails. | + +## Positions + +| Constant | When it runs | +|---|---| +| `Position.INPUT` | Before the user message reaches the agent's LLM | +| `Position.OUTPUT` | After the agent produces a response, before it's returned | + +## OnFail actions + +| Constant | Effect | +|---|---| +| `OnFail.RAISE` | Terminate the agent run with an error (default) | +| `OnFail.RETRY` | Re-run the LLM turn, up to `maxRetries` times | +| `OnFail.FIX` | Ask the LLM to rewrite the output to pass the guardrail | +| `OnFail.HUMAN` | Pause for human review (HITL) | + +## GuardrailResult + +Custom (`func`) guardrails return `GuardrailResult`: + +```java +GuardrailResult.pass() // guardrail passed +GuardrailResult.fail("reason") // guardrail failed +GuardrailResult.fix("rewritten output") // provide a fixed replacement +``` diff --git a/sdk/java/docs/concepts/multi-agent.md b/sdk/java/docs/concepts/multi-agent.md new file mode 100644 index 000000000..29830f9fd --- /dev/null +++ b/sdk/java/docs/concepts/multi-agent.md @@ -0,0 +1,226 @@ +# Multi-Agent + +Agentspan has one primitive — `Agent` — and multiple strategies for composing agents together. Pick the strategy that matches your workflow's structure. + +## Strategy overview + +| Strategy | Description | Use when | +|---|---|---| +| `SEQUENTIAL` | Agents run one after another; output of each feeds the next | Linear pipelines (research → write → edit) | +| `PARALLEL` | All sub-agents run concurrently; results are synthesized | Independent parallel tasks | +| `HANDOFF` | Sub-agent is called as a tool; parent LLM decides when and which | Dynamic routing by LLM | +| `ROUTER` | A dedicated router agent decides which sub-agent runs | Rule-based or LLM-based routing | +| `SWARM` | Any agent can transfer control to another based on triggers | Open-ended conversation routing | +| `ROUND_ROBIN` | Sub-agents take turns in a fixed cycle | Structured multi-agent discussions | +| `RANDOM` | A randomly-selected sub-agent runs each turn | Varied multi-agent discussions | +| `PLAN_EXECUTE` | A planner agent produces a structured plan; steps execute against it | Complex multi-step tasks with dependencies | +| `MANUAL` | No automatic orchestration; you drive the loop | Custom control flow | + +--- + +## Sequential + +```java +Agent researcher = Agent.builder() + .name("researcher").model("openai/gpt-4o-mini") + .instructions("Research the topic and return key facts.") + .build(); + +Agent writer = Agent.builder() + .name("writer").model("openai/gpt-4o-mini") + .instructions("Write a 200-word article from the research notes.") + .build(); + +// Shorthand +Agent pipeline = researcher.then(writer); + +// Equivalent explicit form +Agent pipeline = Agent.builder() + .name("research_pipeline") + .model("openai/gpt-4o-mini") + .agents(researcher, writer) + .strategy(Strategy.SEQUENTIAL) + .build(); + +AgentResult result = runtime.run(pipeline, "Write about the history of jazz"); +``` + +--- + +## Parallel + +Sub-agents run concurrently. A synthesizer (the parent's LLM) combines their outputs. + +```java +Agent french = Agent.builder().name("french_translator").model("openai/gpt-4o-mini") + .instructions("Translate to French.").build(); +Agent spanish = Agent.builder().name("spanish_translator").model("openai/gpt-4o-mini") + .instructions("Translate to Spanish.").build(); +Agent german = Agent.builder().name("german_translator").model("openai/gpt-4o-mini") + .instructions("Translate to German.").build(); + +Agent translator = Agent.builder() + .name("multi_translator") + .model("openai/gpt-4o-mini") + .agents(french, spanish, german) + .strategy(Strategy.PARALLEL) + .synthesize(true) // merge sub-agent outputs + .build(); +``` + +--- + +## Handoff + +Sub-agents are tools the parent's LLM can call. The LLM decides dynamically which agent to invoke and when. + +```java +Agent mathAgent = Agent.builder() + .name("math_agent").model("openai/gpt-4o-mini") + .instructions("Solve math problems.").build(); + +Agent textAgent = Agent.builder() + .name("text_agent").model("openai/gpt-4o-mini") + .instructions("Summarise or rewrite text.").build(); + +Agent dispatcher = Agent.builder() + .name("dispatcher") + .model("openai/gpt-4o-mini") + .instructions("Route requests to the appropriate specialist.") + .agents(mathAgent, textAgent) + .strategy(Strategy.HANDOFF) // default strategy + .build(); +``` + +--- + +## Router + +A dedicated router agent reads the input and selects which sub-agent runs. The router's output is the name of the sub-agent to invoke. + +```java +Agent router = Agent.builder() + .name("intent_router") + .model("openai/gpt-4o-mini") + .instructions("Reply with exactly one word: 'math', 'code', or 'text'.") + .build(); + +Agent parent = Agent.builder() + .name("smart_dispatcher") + .model("openai/gpt-4o-mini") + .router(router) + .agents(mathAgent, codeAgent, textAgent) + .strategy(Strategy.ROUTER) + .build(); +``` + +--- + +## Swarm + +Agents transfer control to each other based on text mentions or tool results. Good for conversational routing. + +```java +import org.conductoross.conductor.ai.handoff.OnTextMention; +import org.conductoross.conductor.ai.handoff.OnToolResult; + +Agent support = Agent.builder() + .name("support_agent") + .model("openai/gpt-4o-mini") + .instructions("Handle general support. Transfer billing issues to billing_agent.") + .handoffs( + OnTextMention.of("billing", "billing_agent"), + OnTextMention.of("refund", "billing_agent"), + OnToolResult.of("escalate_tool", "escalation_agent") + ) + .build(); + +Agent billing = Agent.builder() + .name("billing_agent").model("openai/gpt-4o-mini") + .instructions("Handle billing questions.").build(); + +Agent team = Agent.builder() + .name("support_team") + .model("openai/gpt-4o-mini") + .agents(support, billing) + .strategy(Strategy.SWARM) + .build(); +``` + +### Handoff triggers + +| Class | Factory | Triggers when | +|---|---|---| +| `OnTextMention` | `OnTextMention.of(text, target)` | Agent output contains `text` | +| `OnToolResult` | `OnToolResult.of(toolName, target)` | Tool `toolName` returns any result | +| `OnToolResult` | `OnToolResult.of(toolName, target, contains)` | Tool result contains `contains` | +| `OnCondition` | `new OnCondition(target, predicate)` | Custom Java predicate on the message map | + +--- + +## Plan-Execute + +A planner agent produces a structured `Plan`; a separate executor runs each step. Steps can have dependencies and run in parallel when safe to do so. + +```java +import org.conductoross.conductor.ai.plans.*; + +// Each Op names a tool/worker and passes args. Use new Ref("stepId") to wire a +// later step's input to an earlier step's output. For an LLM-generated step, +// pass a Generate spec: .generate(Generate.builder().instructions("...").build()). +Plan plan = Plan.builder() + .step(Step.builder("fetch_data") + .operation(Op.builder("get_data").args(Map.of("source", "database")).build()) + .build()) + .step(Step.builder("analyse") + .dependsOn("fetch_data") + .operation(Op.builder("analyse_data") + .args(Map.of("rows", new Ref("fetch_data"))) // consumes fetch_data's output + .build()) + .build()) + .step(Step.builder("summarise") + .dependsOn("analyse") + .operation(Op.builder("summarise") + .args(Map.of("analysis", new Ref("analyse"))) + .build()) + .build()) + .build(); + +Agent planExecuteAgent = Agent.builder() + .name("research_pac") + .model("openai/gpt-4o-mini") + .strategy(Strategy.PLAN_EXECUTE) + .build(); + +AgentResult result = runtime.run(planExecuteAgent, "Analyse last month's sales", plan); +``` + +--- + +## Termination conditions + +Stop a multi-agent loop early without hitting `maxTurns`: + +```java +import org.conductoross.conductor.ai.termination.*; + +// Stop after 5 messages +Agent agent = Agent.builder() + .termination(MaxMessageTermination.of(5)) + .build(); + +// Stop when output contains "DONE" +Agent agent = Agent.builder() + .termination(StopMessageTermination.of("DONE")) + .build(); + +// Compose with AND / OR +TerminationCondition cond = MaxMessageTermination.of(10) + .or(StopMessageTermination.of("FINISHED")); + +Agent agent = Agent.builder() + .termination(cond) + .build(); +``` + +See [Termination](termination.md) for the full list. diff --git a/sdk/java/docs/concepts/scheduling.md b/sdk/java/docs/concepts/scheduling.md new file mode 100644 index 000000000..1abc3dee9 --- /dev/null +++ b/sdk/java/docs/concepts/scheduling.md @@ -0,0 +1,88 @@ +# Scheduling + +Run agents on a cron schedule. Schedules are stored in Conductor and survive server restarts — no cron daemon or external scheduler needed. + +## Deploy an agent with a schedule + +```java +import org.conductoross.conductor.ai.schedule.Schedule; + +Agent reportAgent = Agent.builder() + .name("daily_report") + .model("openai/gpt-4o-mini") + .instructions("Generate a daily sales summary.") + .build(); + +Schedule daily = Schedule.builder() + .name("daily") + .cron("0 9 * * *") // 9 AM every day + .timezone("America/New_York") + .build(); + +try (AgentRuntime runtime = new AgentRuntime()) { + runtime.deploy(reportAgent, List.of(daily)); +} +``` + +## Schedule with custom input + +Pass a fixed prompt or parameters to the scheduled run: + +```java +Schedule weeklyDigest = Schedule.builder() + .name("weekly") + .cron("0 8 * * MON") + .input(Map.of("report_type", "weekly", "include_charts", true)) + .description("Monday morning executive digest") + .build(); +``` + +## Manage schedules + +```java +Schedules schedules = runtime.schedules(); + +// List all schedules for an agent +List all = schedules.list("daily_report"); + +// Get a specific schedule by its wire name (agent-name-schedule-name) +ScheduleInfo info = schedules.get("daily_report-daily"); + +// Trigger immediately (ignores cron timing) — runNow takes the ScheduleInfo +String executionId = schedules.runNow(info); + +// Pause and resume (by wire name) +schedules.pause("daily_report-daily"); +schedules.resume("daily_report-daily"); + +// Delete (by wire name) +schedules.delete("daily_report-daily"); + +// Preview the next N fire times for a cron expression +List next = schedules.previewNext("0 9 * * *", 5); // epoch millis +``` + +## Schedule.builder() options + +| Method | Type | Default | Description | +|---|---|---|---| +| `name(String)` | `String` | **required** | Unique name within the agent. | +| `cron(String)` | `String` | **required** | Standard 5-field cron expression. | +| `timezone(String)` | `String` | `"UTC"` | IANA timezone (e.g. `"Europe/London"`). | +| `input(Map)` | `Map` | `{}` | Fixed input passed to the agent on each run. | +| `description(String)` | `String` | `null` | Human-readable description. | +| `paused(boolean)` | `boolean` | `false` | Create in paused state. | +| `catchup(boolean)` | `boolean` | `false` | Run missed executions after a server downtime. | +| `startAt(long)` | `long` | `null` | Epoch milliseconds — schedule not active before this time. | +| `endAt(long)` | `long` | `null` | Epoch milliseconds — schedule disabled after this time. | + +## Cron syntax + +Standard 5-field: `minute hour day-of-month month day-of-week` + +``` +0 9 * * * every day at 9:00 AM +0 */6 * * * every 6 hours +0 8 * * MON-FRI weekdays at 8 AM +30 17 1 * * 1st of every month at 5:30 PM +``` diff --git a/sdk/java/docs/concepts/skills.md b/sdk/java/docs/concepts/skills.md new file mode 100644 index 000000000..27d6985ac --- /dev/null +++ b/sdk/java/docs/concepts/skills.md @@ -0,0 +1,97 @@ +# Skills + +A Skill is a portable, self-contained agent capability stored as a directory. You write a `SKILL.md` file describing the agent's purpose and workflow; the SDK loads it as a fully-configured `Agent` ready to run. + +## Skill directory layout + +``` +my-skill/ +├── SKILL.md # Required — name, description, workflow instructions +├── search-agent.md # Optional — sub-agent definitions +├── writer-agent.md +└── scripts/ # Optional — scripts the agent can execute + └── process.py +``` + +## SKILL.md format + +```markdown +--- +name: code_review +params: + language: + default: java +--- + +## Overview +Reviews code for bugs, style issues, and security vulnerabilities. + +## Workflow +1. Read the code from the user's message. +2. Call the analyse_code tool with the code and language. +3. Return a structured review with severity levels. +``` + +## Loading a skill + +```java +import org.conductoross.conductor.ai.skill.Skill; +import java.nio.file.Paths; + +// Load a single skill +Agent reviewAgent = Skill.skill(Paths.get("skills/code-review"), "openai/gpt-4o"); + +// Override sub-agent models +Agent reviewAgent = Skill.skill( + Paths.get("skills/code-review"), + "openai/gpt-4o", + Map.of("search-agent", "openai/gpt-4o-mini") // cheaper model for search +); + +// Load all skills from a directory +Map allSkills = Skill.loadSkills(Paths.get("skills"), "openai/gpt-4o"); + +// Run a loaded skill +try (AgentRuntime runtime = new AgentRuntime()) { + AgentResult result = runtime.run(reviewAgent, "Review this Java code: ..."); +} +``` + +## Sub-agent files (`*-agent.md`) + +Each `*-agent.md` file defines a sub-agent within the skill: + +```markdown +--- +name: analyse_code +description: Analyse code for issues +--- + +You are a code analysis specialist. When given code: +1. Check for common bugs and anti-patterns. +2. Identify security vulnerabilities. +3. Return a JSON list of findings with severity (low/medium/high). +``` + +## Error handling + +```java +import org.conductoross.conductor.ai.skill.SkillLoadError; + +try { + Agent skill = Skill.skill(Paths.get("skills/missing"), "openai/gpt-4o"); +} catch (SkillLoadError e) { + System.err.println("Failed to load skill: " + e.getMessage()); +} +``` + +## Publishing and sharing skills + +Skills are plain files — commit them to your repo, share them via Git, or distribute as JARs. The `Skill.skill()` loader accepts any `Path`, including paths inside JARs via `FileSystem`: + +```java +// Load a skill bundled inside a JAR on the classpath +try (var fs = FileSystems.newFileSystem(uri, Map.of())) { + Agent bundledSkill = Skill.skill(fs.getPath("/skills/my-skill"), "openai/gpt-4o"); +} +``` diff --git a/sdk/java/docs/concepts/termination.md b/sdk/java/docs/concepts/termination.md new file mode 100644 index 000000000..d08047f65 --- /dev/null +++ b/sdk/java/docs/concepts/termination.md @@ -0,0 +1,80 @@ +# Termination Conditions + +Termination conditions stop a multi-agent loop before it hits `maxTurns`. They are particularly useful in swarm and conversation patterns where you don't know upfront how many turns are needed. + +## Available conditions + +### MaxMessageTermination + +Stop after a fixed number of messages: + +```java +import org.conductoross.conductor.ai.termination.MaxMessageTermination; + +Agent agent = Agent.builder() + .termination(MaxMessageTermination.of(10)) + .build(); +``` + +### StopMessageTermination + +Stop when the agent outputs a specific string: + +```java +import org.conductoross.conductor.ai.termination.StopMessageTermination; + +Agent agent = Agent.builder() + .termination(StopMessageTermination.of("TASK_COMPLETE")) + .build(); +``` + +Instruct the agent in its system prompt: + +``` +When you have finished the task, output exactly: TASK_COMPLETE +``` + +### TextMentionTermination + +Stop when the output mentions specific text (optionally case-sensitive): + +```java +import org.conductoross.conductor.ai.termination.TextMentionTermination; + +// Case-insensitive (default) +TextMentionTermination.of("done") + +// Case-sensitive +TextMentionTermination.of("DONE", true) +``` + +### TokenUsageTermination + +Stop when cumulative token usage exceeds a limit — useful for cost control: + +```java +import org.conductoross.conductor.ai.termination.TokenUsageTermination; + +// Stop after 50,000 total tokens +Agent agent = Agent.builder() + .termination(TokenUsageTermination.ofTotal(50_000)) + .build(); +``` + +## Composing conditions + +Chain conditions with `and()` / `or()`: + +```java +// Stop when BOTH are true: max 20 messages AND output contains "DONE" +TerminationCondition both = MaxMessageTermination.of(20) + .and(StopMessageTermination.of("DONE")); + +// Stop when EITHER is true: max 20 messages OR output contains "DONE" +TerminationCondition either = MaxMessageTermination.of(20) + .or(StopMessageTermination.of("DONE")); + +Agent agent = Agent.builder() + .termination(either) + .build(); +``` diff --git a/sdk/java/docs/concepts/tools.md b/sdk/java/docs/concepts/tools.md new file mode 100644 index 000000000..76072c970 --- /dev/null +++ b/sdk/java/docs/concepts/tools.md @@ -0,0 +1,234 @@ +# Tools + +Tools give agents the ability to take actions. In Agentspan, each tool invocation runs as a Conductor task — distributed, retryable, and observable in the workflow audit log. + +## Java method tools (`@Tool`) + +Annotate methods with `@Tool` and convert the containing object to tools with `ToolRegistry.fromInstance()` (returns a `List`, one per annotated method): + +```java +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.ToolContext; + +public class SearchTools { + + @Tool(name = "web_search", description = "Search the web for current information") + public String search(String query) { + return callSearchApi(query); + } + + @Tool(name = "get_page", description = "Fetch the content of a URL") + public String getPage(String url) { + return fetchUrl(url); + } +} + +Agent agent = Agent.builder() + .name("research_agent") + .model("openai/gpt-4o-mini") + .tools(ToolRegistry.fromInstance(new SearchTools())) + .build(); +``` + +### Tool parameters + +The LLM sees a JSON Schema built from the method signature. Supported parameter types: `String`, `int`/`Integer`, `long`/`Long`, `double`/`Double`, `boolean`/`Boolean`, `List`, `Map`, and any `record` or POJO with public getters. + +```java +@Tool(name = "create_issue", description = "Create a GitHub issue") +public String createIssue( + String title, + String body, + List labels +) { + // ... +} +``` + +### ToolContext + +Inject `ToolContext` as the last parameter to access execution metadata, session state, and credentials: + +```java +@Tool(name = "send_email", description = "Send an email") +public String sendEmail(String to, String subject, String body, ToolContext ctx) { + String apiKey = Credentials.get("SENDGRID_API_KEY"); + String executionId = ctx.getExecutionId(); + // ... +} +``` + +### Credentials in tools + +Declare which secrets a tool needs via `Agent.builder().credentials(...)`. The SDK fetches them from the Agentspan secrets store and injects them at runtime: + +```java +Agent agent = Agent.builder() + .name("github_agent") + .credentials("GITHUB_TOKEN") + .tools(ToolRegistry.fromInstance(new GitHubTools())) + .build(); + +// Store the secret once via the CLI or API: +// agentspan secrets set GITHUB_TOKEN ghp_xxxxx +``` + +--- + +## HTTP tools + +Call any REST endpoint without writing Java code: + +```java +import org.conductoross.conductor.ai.tools.HttpTool; + +ToolDef searchTool = HttpTool.builder() + .name("search") + .description("Search for products") + .url("https://api.mystore.com/search") + .method("GET") + .build(); + +Agent agent = Agent.builder() + .name("shop_agent") + .model("openai/gpt-4o-mini") + .tools(searchTool) + .build(); +``` + +--- + +## MCP tools + +Connect to any [Model Context Protocol](https://modelcontextprotocol.io) server: + +```java +import org.conductoross.conductor.ai.tools.McpTool; + +ToolDef mcpTool = McpTool.builder() + .name("filesystem") + .description("Access the local filesystem via MCP") + .serverUrl("http://localhost:3001") + .build(); +``` + +--- + +## CLI tools + +Run shell commands as tool calls. The command runs in your local process; the agent decides the arguments. + +```java +import org.conductoross.conductor.ai.execution.CliConfig; + +Agent agent = Agent.builder() + .name("devops_agent") + .model("openai/gpt-4o-mini") + .instructions("Run git commands as requested.") + .cliConfig(CliConfig.builder() + .allowedCommands(List.of("git status", "git log", "git diff")) + .timeout(30) + .build()) + .build(); +``` + +!!! warning "Security" + Use `allowedCommands` to restrict which commands the agent can execute. Without a whitelist, the agent can run any command the JVM user has permission to execute. + +--- + +## Human-in-the-loop tools + +Pause the agent and wait for a human decision: + +```java +import org.conductoross.conductor.ai.tools.HumanTool; + +ToolDef approvalTool = HumanTool.create( + "approve_deployment", + "Request human approval before deploying to production" +); + +Agent agent = Agent.builder() + .name("deploy_agent") + .model("openai/gpt-4o-mini") + .tools(approvalTool) + .build(); +``` + +When the agent calls this tool, execution pauses. Resume it with: + +```java +AgentHandle handle = runtime.start(agent, "Deploy version 2.1 to production"); + +// Later, once a human decides: +handle.approve("Approved by Alice"); +// or +handle.reject("Needs more testing"); +``` + +The workflow can wait days — it's stored durably in Conductor. + +--- + +## PDF generation + +```java +import org.conductoross.conductor.ai.tools.PdfTool; + +ToolDef pdfTool = PdfTool.create("generate_report", "Generate a formatted PDF report"); +``` + +--- + +## Image / media tools + +```java +import org.conductoross.conductor.ai.tools.MediaTools; + +ToolDef imageTool = MediaTools.imageTool( + "generate_image", + "Generate an image from a description", + "openai", + "dall-e-3" +); +``` + +--- + +## Async message tools + +Wait for an external event before continuing: + +```java +import org.conductoross.conductor.ai.tools.WaitForMessageTool; + +ToolDef waitTool = WaitForMessageTool.create( + "wait_for_payment", + "Wait until the payment webhook confirms the transaction" +); +``` + +--- + +## Agent tools (sub-agents) + +Any `Agent` can be a tool for another agent. This is the building block for all multi-agent patterns: + +```java +Agent researcher = Agent.builder() + .name("researcher") + .model("openai/gpt-4o-mini") + .instructions("Research a topic and return a summary.") + .build(); + +Agent writer = Agent.builder() + .name("writer") + .model("openai/gpt-4o-mini") + .instructions("Write an article given a research summary.") + .agents(researcher) // researcher becomes a callable tool + .build(); +``` + +See [Multi-Agent](multi-agent.md) for orchestration patterns. diff --git a/sdk/java/docs/frameworks/google-adk.md b/sdk/java/docs/frameworks/google-adk.md new file mode 100644 index 000000000..f20f1042c --- /dev/null +++ b/sdk/java/docs/frameworks/google-adk.md @@ -0,0 +1,82 @@ +# Google ADK + +Use Google's Agent Development Kit (ADK) agents directly with Agentspan. The `AdkBridge` converts a native `LlmAgent` (or any `BaseAgent`) into an Agentspan `Agent`, serialising its tools, instructions, and sub-agent graph into the format the server's `GoogleADKNormalizer` understands. + +## Dependency + +```groovy +implementation 'org.conductoross.conductor:conductor-ai-sdk:0.1.0' +compileOnly 'com.google.adk:google-adk:1.3.0' +``` + +## Usage + +```java +import com.google.adk.agents.LlmAgent; +import com.google.adk.tools.Annotations; +import com.google.adk.tools.FunctionTool; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.frameworks.AdkBridge; + +// A FunctionTool target — ADK reflects the method and its @Schema params +public static class WeatherService { + public static Map getWeather( + @Annotations.Schema(name = "city", description = "City to query") String city) { + return Map.of("city", city, "condition", "Sunny", "tempC", 22); + } +} + +// Build a native ADK LlmAgent +LlmAgent adkAgent = LlmAgent.builder() + .name("weather_agent") + .model("gemini-2.0-flash") + .instruction("Answer weather questions. Use the getWeather tool.") + .tools(FunctionTool.create(WeatherService.class, "getWeather")) + .build(); + +// Convert to Agentspan Agent +Agent agent = AdkBridge.toAgentspan(adkAgent); + +// Run via AgentRuntime +try (AgentRuntime runtime = new AgentRuntime()) { + AgentResult result = runtime.run(agent, "What's the weather in London?"); + System.out.println(result.getOutput()); +} +``` + +## agentBuilder — attach extra Agentspan features + +If you want to mix ADK agent structure with Agentspan-only features (guardrails, credentials, callbacks), use `agentBuilder()` which returns an `Agent.Builder` you can continue configuring: + +```java +import org.conductoross.conductor.ai.guardrail.RegexGuardrail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.enums.OnFail; + +Agent agent = AdkBridge.agentBuilder(adkAgent) + .credentials("WEATHER_API_KEY") + .guardrails(RegexGuardrail.builder() + .name("no_pii").position(Position.OUTPUT) + .patterns("\\b\\d{3}-\\d{2}-\\d{4}\\b") // SSN-like + .onFail(OnFail.RAISE).build()) + .maxTurns(10) + .build(); +``` + +## What gets mapped + +| ADK concept | Agentspan mapping | +|---|---| +| `LlmAgent.name()` | `Agent.name` | +| `LlmAgent.model()` | `Agent.model` | +| `LlmAgent.instruction()` | `Agent.instructions` | +| `FunctionTool` | Conductor worker task (via `WorkerManager`) | +| `AgentTool` | Sub-agent (nested `Agent`) | +| `GoogleSearchTool` | HTTP tool | +| `BuiltInCodeExecutionTool` | Code execution tool | +| Sub-agents (`.subAgents()`) | `Agent.agents` | +| `LoopAgent` / `SequentialAgent` | `Strategy.SEQUENTIAL` | + +!!! note "Model requirement" + Google ADK agents require a Gemini model (e.g. `gemini-2.0-flash`). Make sure your Agentspan server has a Google AI or Vertex AI provider configured with the appropriate API key. diff --git a/sdk/java/docs/frameworks/langchain4j.md b/sdk/java/docs/frameworks/langchain4j.md new file mode 100644 index 000000000..06f1970a9 --- /dev/null +++ b/sdk/java/docs/frameworks/langchain4j.md @@ -0,0 +1,83 @@ +# LangChain4j + +Use LangChain4j `@Tool`-annotated POJOs directly with Agentspan. The bridge reflects your annotated methods, builds a JSON Schema from the parameter types, and registers each method as a Conductor worker task. + +## Dependency + +```groovy +implementation 'org.conductoross.conductor:conductor-ai-sdk:0.1.0' +compileOnly 'dev.langchain4j:langchain4j:1.0.0' +``` + +## Usage + +```java +import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.agent.tool.P; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.frameworks.LangChain4jAgent; + +// Your existing LangChain4j tool POJO — no changes needed +public class CalculatorTools { + + @Tool("Add two integers and return the result") + public int add(@P("a") int a, @P("b") int b) { + return a + b; + } + + @Tool("Look up the current stock price for a ticker symbol") + public double stockPrice(@P("ticker") String ticker) { + return fetchPrice(ticker); + } +} + +// Wrap with LangChain4jAgent +Agent agent = LangChain4jAgent.from( + "calculator_agent", // agent name + "openai/gpt-4o-mini", // model + "You can perform math and look up prices.", // instructions + new CalculatorTools() // one or more tool POJOs +); + +try (AgentRuntime runtime = new AgentRuntime()) { + AgentResult result = runtime.run(agent, "What is 7 plus 8?"); + System.out.println(result.getOutput()); +} +``` + +## Detection + +Check whether an object has LangChain4j `@Tool` methods: + +```java +boolean isTools = LangChain4jAgent.isLangChain4jTools(new CalculatorTools()); // true +boolean isTools = LangChain4jAgent.isLangChain4jTools(new Object()); // false +``` + +## What gets mapped + +| LangChain4j annotation | Agentspan mapping | +|---|---| +| `@Tool("description")` | Tool name = method name; description = annotation value | +| `@Tool(name="x", value="desc")` | Tool name = `x`; description = `desc` | +| `@P("paramName")` | JSON Schema property name | +| Method return type | Output schema | + +## Using with LangChainBridge + +For `ChatModel`-based agents (not `@Tool` POJOs): + +```java +import dev.langchain4j.model.chat.ChatModel; +import org.conductoross.conductor.ai.frameworks.LangChainBridge; + +ChatModel model = OpenAiChatModel.builder() + .apiKey(System.getenv("OPENAI_API_KEY")) + .modelName("gpt-4o-mini") + .build(); + +Agent agent = LangChainBridge.agentBuilder("lc_agent", model, "You are helpful.") + .tools(ToolRegistry.fromInstance(new SearchTools())) + .build(); +``` diff --git a/sdk/java/docs/frameworks/openai.md b/sdk/java/docs/frameworks/openai.md new file mode 100644 index 000000000..51543134c --- /dev/null +++ b/sdk/java/docs/frameworks/openai.md @@ -0,0 +1,91 @@ +# OpenAI Agents SDK + +Use the Agentspan Java SDK with OpenAI Agents SDK-style tool definitions. The `OpenAIAgent` bridge accepts `@Tool`-annotated POJOs and registers them as Conductor worker tasks, routing the agent through the server's `OpenAINormalizer`. + +## Dependency + +```groovy +implementation 'org.conductoross.conductor:conductor-ai-sdk:0.1.0' +``` + +The bridge uses the LangChain4j `@Tool` annotation as a practical equivalent of the Python OpenAI Agents SDK `@function_tool` decorator — add it if you need the annotation: + +```groovy +compileOnly 'dev.langchain4j:langchain4j:1.0.0' +``` + +## Usage + +```java +import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.agent.tool.P; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.frameworks.OpenAIAgent; + +public class ShoppingTools { + + @Tool(name = "search_products", value = "Search for products by keyword") + public String searchProducts(@P("query") String query, @P("maxResults") int maxResults) { + return callSearchApi(query, maxResults); + } + + @Tool(name = "add_to_cart", value = "Add a product to the shopping cart") + public String addToCart(@P("productId") String productId, @P("quantity") int quantity) { + return cartService.add(productId, quantity); + } +} + +Agent agent = OpenAIAgent.builder() + .name("shopping_assistant") + .model("openai/gpt-4o-mini") + .instructions("Help users find and purchase products.") + .tools(new ShoppingTools()) + .build(); + +try (AgentRuntime runtime = new AgentRuntime()) { + AgentResult result = runtime.run(agent, "Find me a blue jacket under $100"); + System.out.println(result.getOutput()); +} +``` + +## Handoffs + +OpenAI Agents SDK-style handoffs let the LLM transfer control to a specialist agent: + +```java +Agent billingAgent = Agent.builder() + .name("billing_agent") + .model("openai/gpt-4o-mini") + .instructions("Handle billing and payment questions.") + .build(); + +Agent supportAgent = OpenAIAgent.builder() + .name("support_agent") + .model("openai/gpt-4o-mini") + .instructions("Handle general support. Transfer billing issues to the billing agent.") + .handoffs(billingAgent) // adds billing_agent as a handoff target + .build(); +``` + +## Structured output + +```java +Agent agent = OpenAIAgent.builder() + .name("classifier") + .model("openai/gpt-4o-mini") + .instructions("Classify the sentiment of the input.") + .outputType("SentimentResult") // server-side structured output type name + .build(); +``` + +## Builder reference + +| Method | Description | +|---|---| +| `name(String)` | **Required.** Agent and workflow name. | +| `model(String)` | **Required.** `"provider/model"` string. | +| `instructions(String)` | System prompt. | +| `tools(Object...)` | `@Tool`-annotated POJOs; each method becomes a worker task. | +| `handoffs(Agent...)` | Sub-agents the LLM can hand off to. | +| `outputType(String)` | Structured output type name for the server normalizer. | diff --git a/sdk/java/docs/generated/AgentConfigModel.java b/sdk/java/docs/generated/AgentConfigModel.java new file mode 100644 index 000000000..68f2de628 --- /dev/null +++ b/sdk/java/docs/generated/AgentConfigModel.java @@ -0,0 +1,25 @@ +// AUTO-GENERATED from agent-schema.json by generate.py — do not edit. +import java.util.List; +import java.util.Map; + +public final class AgentConfigModel { + private AgentConfigModel() {} + + public record AgentConfig(String name, String description, String model, Boolean external, String baseUrl, Object instructions, String introduction, List tools, List agents, String strategy, Object router, List guardrails, Integer maxTurns, Integer maxTokens, Double temperature, Integer timeoutSeconds, String reasoningEffort, Integer contextWindowBudget, ThinkingConfig thinkingConfig, Memory memory, Termination termination, OutputType outputType, List handoffs, Map allowedTransitions, List callbacks, Gate gate, WorkerRef stopWhen, Boolean enablePlanning, AgentConfig planner, AgentConfig fallback, Integer fallbackMaxTurns, List plannerContext, Map planSource, Boolean synthesize, Boolean stateful, String sessionId, String includeContents, List requiredTools, List prefillTools, List credentials, Map metadata, Boolean localCodeExecution, CodeExecution codeExecution, CliConfig cliConfig, List maskedFields) {} + public record PromptTemplate(String type, String name, Map variables, Integer version) {} + public record Tool(String name, String description, Map inputSchema, Map outputSchema, String toolType, Boolean approvalRequired, Boolean stateful, Integer timeoutSeconds, Integer maxCalls, Map config, List guardrails) {} + public record Guardrail(String name, String guardrailType, String position, String onFail, Integer maxRetries, String taskName, List patterns, String mode, String message, String model, String policy, Integer maxTokens) {} + public record Termination(String type, String text, Boolean caseSensitive, String stopMessage, Integer maxMessages, Integer maxTotalTokens, Integer maxPromptTokens, Integer maxCompletionTokens, List conditions) {} + public record Handoff(String type, String target, String toolName, String resultContains, String text, String taskName) {} + public record Callback(String position, String taskName) {} + public record Memory(List messages, Integer maxMessages) {} + public record Message(String role, String message) {} + public record CodeExecution(Boolean enabled, List allowedLanguages, List allowedCommands, Integer timeout) {} + public record CliConfig(Boolean enabled, List allowedCommands, Integer timeout, Boolean allowShell, String workingDir) {} + public record ThinkingConfig(Boolean enabled, Integer budgetTokens) {} + public record PrefillTool(String toolName, Map arguments) {} + public record PlannerContextEntry(String text, String url, Map headers, Boolean required, Integer maxBytes) {} + public record OutputType(Map schema, String className) {} + public record Gate(String type, String text, Boolean caseSensitive, String taskName) {} + public record WorkerRef(String taskName) {} +} diff --git a/sdk/java/docs/generated/agent_config.py b/sdk/java/docs/generated/agent_config.py new file mode 100644 index 000000000..ad63cf98d --- /dev/null +++ b/sdk/java/docs/generated/agent_config.py @@ -0,0 +1,190 @@ +"""AUTO-GENERATED from agent-schema.json by generate.py — do not edit.""" +from __future__ import annotations +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + + +@dataclass +class AgentConfig: + name: Optional[str] = None + description: Optional[str] = None + model: Optional[str] = None + external: Optional[bool] = None + baseUrl: Optional[str] = None + instructions: Optional[Any] = None + introduction: Optional[str] = None + tools: Optional[List[Tool]] = None + agents: Optional[List[AgentConfig]] = None + strategy: Optional[str] = None + router: Optional[Any] = None + guardrails: Optional[List[Guardrail]] = None + maxTurns: Optional[int] = None + maxTokens: Optional[int] = None + temperature: Optional[float] = None + timeoutSeconds: Optional[int] = None + reasoningEffort: Optional[str] = None + contextWindowBudget: Optional[int] = None + thinkingConfig: Optional[ThinkingConfig] = None + memory: Optional[Memory] = None + termination: Optional[Termination] = None + outputType: Optional[OutputType] = None + handoffs: Optional[List[Handoff]] = None + allowedTransitions: Optional[Dict[str, Any]] = None + callbacks: Optional[List[Callback]] = None + gate: Optional[Gate] = None + stopWhen: Optional[WorkerRef] = None + enablePlanning: Optional[bool] = None + planner: Optional[AgentConfig] = None + fallback: Optional[AgentConfig] = None + fallbackMaxTurns: Optional[int] = None + plannerContext: Optional[List[PlannerContextEntry]] = None + planSource: Optional[Dict[str, Any]] = None + synthesize: Optional[bool] = None + stateful: Optional[bool] = None + sessionId: Optional[str] = None + includeContents: Optional[str] = None + requiredTools: Optional[List[str]] = None + prefillTools: Optional[List[PrefillTool]] = None + credentials: Optional[List[str]] = None + metadata: Optional[Dict[str, Any]] = None + localCodeExecution: Optional[bool] = None + codeExecution: Optional[CodeExecution] = None + cliConfig: Optional[CliConfig] = None + maskedFields: Optional[List[str]] = None + + +@dataclass +class PromptTemplate: + type: Optional[str] = None + name: Optional[str] = None + variables: Optional[Dict[str, Any]] = None + version: Optional[int] = None + + +@dataclass +class Tool: + name: Optional[str] = None + description: Optional[str] = None + inputSchema: Optional[Dict[str, Any]] = None + outputSchema: Optional[Dict[str, Any]] = None + toolType: Optional[str] = None + approvalRequired: Optional[bool] = None + stateful: Optional[bool] = None + timeoutSeconds: Optional[int] = None + maxCalls: Optional[int] = None + config: Optional[Dict[str, Any]] = None + guardrails: Optional[List[Guardrail]] = None + + +@dataclass +class Guardrail: + name: Optional[str] = None + guardrailType: Optional[str] = None + position: Optional[str] = None + onFail: Optional[str] = None + maxRetries: Optional[int] = None + taskName: Optional[str] = None + patterns: Optional[List[str]] = None + mode: Optional[str] = None + message: Optional[str] = None + model: Optional[str] = None + policy: Optional[str] = None + maxTokens: Optional[int] = None + + +@dataclass +class Termination: + type: Optional[str] = None + text: Optional[str] = None + caseSensitive: Optional[bool] = None + stopMessage: Optional[str] = None + maxMessages: Optional[int] = None + maxTotalTokens: Optional[int] = None + maxPromptTokens: Optional[int] = None + maxCompletionTokens: Optional[int] = None + conditions: Optional[List[Termination]] = None + + +@dataclass +class Handoff: + type: Optional[str] = None + target: Optional[str] = None + toolName: Optional[str] = None + resultContains: Optional[str] = None + text: Optional[str] = None + taskName: Optional[str] = None + + +@dataclass +class Callback: + position: Optional[str] = None + taskName: Optional[str] = None + + +@dataclass +class Memory: + messages: Optional[List[Message]] = None + maxMessages: Optional[int] = None + + +@dataclass +class Message: + role: Optional[str] = None + message: Optional[str] = None + + +@dataclass +class CodeExecution: + enabled: Optional[bool] = None + allowedLanguages: Optional[List[str]] = None + allowedCommands: Optional[List[str]] = None + timeout: Optional[int] = None + + +@dataclass +class CliConfig: + enabled: Optional[bool] = None + allowedCommands: Optional[List[str]] = None + timeout: Optional[int] = None + allowShell: Optional[bool] = None + workingDir: Optional[str] = None + + +@dataclass +class ThinkingConfig: + enabled: Optional[bool] = None + budgetTokens: Optional[int] = None + + +@dataclass +class PrefillTool: + toolName: Optional[str] = None + arguments: Optional[Dict[str, Any]] = None + + +@dataclass +class PlannerContextEntry: + text: Optional[str] = None + url: Optional[str] = None + headers: Optional[Dict[str, Any]] = None + required: Optional[bool] = None + maxBytes: Optional[int] = None + + +@dataclass +class OutputType: + schema: Optional[Dict[str, Any]] = None + className: Optional[str] = None + + +@dataclass +class Gate: + type: Optional[str] = None + text: Optional[str] = None + caseSensitive: Optional[bool] = None + taskName: Optional[str] = None + + +@dataclass +class WorkerRef: + taskName: Optional[str] = None diff --git a/sdk/java/docs/generated/generate.py b/sdk/java/docs/generated/generate.py new file mode 100644 index 000000000..6a94cce92 --- /dev/null +++ b/sdk/java/docs/generated/generate.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Reverse-engineer agent-schema.json into a Python dataclass and a Java record, +then prove the schema is correct by diffing the generated models against the +server's AgentConfig models and validating a generated instance. + +Run from the repo root: python3 sdk/java/docs/generated/generate.py +Outputs (regenerated each run): + sdk/java/docs/generated/agent_config.py — Python dataclasses + sdk/java/docs/generated/AgentConfigModel.java — Java records + +Exit code 0 iff: (a) generated models are field-for-field identical to the +schema, (b) a generated instance validates against the schema, and (c) every +server AgentConfig field (root + nested models) is present in the schema. +""" +import json, os, re, sys, importlib.util, dataclasses + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(HERE, "..", "..", "..", "..")) +SCHEMA = os.path.join(ROOT, "sdk/java/docs/agent-schema.json") +MODEL_DIR = os.path.join(ROOT, "server/conductor-agentspan/src/main/java/dev/agentspan/runtime/model") + +schema = json.load(open(SCHEMA)) +defs = schema["$defs"] +ROOT_CLS = "AgentConfig" +cap = lambda d: d[0].upper() + d[1:] +ref_cls = lambda r: ROOT_CLS if r == "#" else cap(r.split("/")[-1]) + +PY = {"string": "str", "integer": "int", "number": "float", "boolean": "bool", "object": "Dict[str, Any]"} +JV = {"string": "String", "integer": "Integer", "number": "Double", "boolean": "Boolean", "object": "Map"} + +def base_type(s, scalar, ref, arr): + if not isinstance(s, dict): return scalar(None) + if "$ref" in s: return ref(s["$ref"]) + if "oneOf" in s: return scalar("__any__") + t = s.get("type") + if isinstance(t, list): + nn = [x for x in t if x != "null"] + return base_type({"type": nn[0]} if nn else {}, scalar, ref, arr) if nn else scalar("__any__") + if t == "array": return arr(s.get("items", {})) + return scalar(t) + +py_type = lambda s: base_type(s, lambda t: ("Any" if t in (None, "__any__") else PY.get(t, "Any")), + ref_cls, lambda it: f"List[{py_type(it)}]") +jv_type = lambda s: base_type(s, lambda t: ("Object" if t in (None, "__any__") else JV.get(t, "Object")), + ref_cls, lambda it: f"List<{jv_type(it)}>") + +order = [(ROOT_CLS, schema)] + [(cap(n), d) for n, d in defs.items()] + +def gen_py(name, node): + p = node.get("properties") or {} + if not p: return f"@dataclass\nclass {name}:\n pass" + body = "\n".join(f" {k}: Optional[{py_type(v)}] = None" for k, v in p.items()) + return f"@dataclass\nclass {name}:\n{body}" + +def gen_java(name, node): + p = node.get("properties") or {} + if not p: return f" public record {name}() {{}}" + comps = ", ".join(f"{jv_type(v)} {k}" for k, v in p.items()) + return f" public record {name}({comps}) {{}}" + +with open(os.path.join(HERE, "agent_config.py"), "w") as f: + f.write('"""AUTO-GENERATED from agent-schema.json by generate.py — do not edit."""\n') + f.write("from __future__ import annotations\nfrom dataclasses import dataclass\n") + f.write("from typing import Any, Dict, List, Optional\n\n\n") + f.write("\n\n\n".join(gen_py(n, d) for n, d in order) + "\n") + +with open(os.path.join(HERE, "AgentConfigModel.java"), "w") as f: + f.write("// AUTO-GENERATED from agent-schema.json by generate.py — do not edit.\n") + f.write("import java.util.List;\nimport java.util.Map;\n\npublic final class AgentConfigModel {\n") + f.write(" private AgentConfigModel() {}\n\n") + f.write("\n".join(gen_java(n, d) for n, d in order) + "\n}\n") + +# ---- proof ---- +spec = importlib.util.spec_from_file_location("gen_ac", os.path.join(HERE, "agent_config.py")) +m = importlib.util.module_from_spec(spec); sys.modules["gen_ac"] = m; spec.loader.exec_module(m) + +ok = True + +# (a) generated models are field-for-field identical to the schema +for clsname, node in [(ROOT_CLS, schema)] + [(cap(n), defs[n]) for n in defs]: + fields = {x.name for x in dataclasses.fields(getattr(m, clsname))} + props = set((node.get("properties") or {}).keys()) + if fields != props: + ok = False; print(f"[a] FAIL {clsname}: {fields ^ props}") +print("[a] generated dataclass/record fields ≡ schema (root + %d nested): %s" % (len(defs), "OK" if ok else "FAIL")) + +# (b) a generated instance validates against the schema +try: + from jsonschema import Draft202012Validator + clean = lambda x: ({k: clean(v) for k, v in x.items() if v is not None} if isinstance(x, dict) + else [clean(v) for v in x] if isinstance(x, list) else x) + inst = m.AgentConfig(name="gen_agent", model="openai/gpt-4o", external=False, maxTurns=5, + timeoutSeconds=0, reasoningEffort="high", + memory=m.Memory(messages=[{"role": "user", "message": "hi"}], maxMessages=10), + gate=m.Gate(type="text_contains", text="STOP", caseSensitive=False)) + errs = list(Draft202012Validator(schema).iter_errors(clean(dataclasses.asdict(inst)))) + for e in errs: ok = False; print(" VIOLATION:", e.message) + print("[b] generated instance validates against schema:", "OK" if not errs else "FAIL") +except ImportError: + print("[b] skipped (pip install jsonschema to run)") + +# (c) every server AgentConfig field (root + nested) is present in the schema +def server_fields(cls): + p = os.path.join(MODEL_DIR, cls + ".java") + if not os.path.exists(p): return None + src = open(p).read() + fields = re.findall(r"private\s+(?:final\s+)?[\w<>,\s\[\].]+?\s+([a-z]\w*)\s*[;=]", src) + jp = dict(re.findall(r'@JsonProperty\("([^"]+)"\)[^;{]*?\s(\w+)\s*[;=]', src)) + inv = {v: k for k, v in jp.items()} + return {inv.get(f, f) for f in fields} + +MAP = {ROOT_CLS: "AgentConfig", "Tool": "ToolConfig", "Guardrail": "GuardrailConfig", + "Memory": "MemoryConfig", "Termination": "TerminationConfig", "Handoff": "HandoffConfig", + "Callback": "CallbackConfig", "CodeExecution": "CodeExecutionConfig", "CliConfig": "CliConfig", + "ThinkingConfig": "ThinkingConfig", "PrefillTool": "PrefillToolCallConfig", + "OutputType": "OutputTypeConfig", "WorkerRef": "WorkerRef"} +gaps = {} +for clsname, server_cls in MAP.items(): + node = schema if clsname == ROOT_CLS else defs[clsname[0].lower() + clsname[1:]] + sf = server_fields(server_cls) + if sf is None: continue + miss = sf - set((node.get("properties") or {}).keys()) + if miss: gaps[clsname] = sorted(miss); ok = False +print("[c] every server field present in schema:", "OK" if not gaps else f"GAPS {gaps}") + +print("\nRESULT:", "SCHEMA VERIFIED ✅" if ok else "VERIFICATION FAILED ❌") +sys.exit(0 if ok else 1) diff --git a/sdk/java/docs/getting-started.md b/sdk/java/docs/getting-started.md new file mode 100644 index 000000000..fe08ab163 --- /dev/null +++ b/sdk/java/docs/getting-started.md @@ -0,0 +1,136 @@ +# Getting Started + +## Prerequisites + +- Java 21+ +- Gradle 7+ or Maven 3.6+ +- A running Agentspan server — see [self-hosting](../self-hosting.md) or start one locally: + +```bash +docker run -p 6767:6767 agentspan/server:latest +``` + +## Add the dependency + +=== "Gradle" + + ```groovy + dependencies { + implementation 'org.conductoross.conductor:conductor-ai-sdk:0.1.0' + } + ``` + +=== "Maven" + + ```xml + + org.conductoross.conductor + conductor-ai-sdk + 0.1.0 + + ``` + +## Configure the connection + +The SDK reads connection settings from environment variables by default: + +```bash +export AGENTSPAN_SERVER_URL=http://localhost:6767 # default +export AGENTSPAN_AUTH_KEY=your-key # optional +export AGENTSPAN_AUTH_SECRET=your-secret # optional +export AGENTSPAN_LLM_MODEL=openai/gpt-4o-mini # default model +``` + +Or construct an `ApiClient` explicitly: + +```java +import io.orkes.conductor.client.ApiClient; +import org.conductoross.conductor.ai.AgentRuntime; + +// No auth (local dev) +ApiClient client = AgentRuntime.client("http://localhost:6767"); + +// With key/secret +ApiClient client = AgentRuntime.client("http://myserver:6767", "key", "secret"); + +AgentRuntime runtime = new AgentRuntime(client); +``` + +## Run your first agent + +```java +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; + +Agent agent = Agent.builder() + .name("hello_agent") + .model("openai/gpt-4o-mini") + .instructions("You are a concise assistant. Answer in one sentence.") + .build(); + +try (AgentRuntime runtime = new AgentRuntime()) { + AgentResult result = runtime.run(agent, "What is 2 + 2?"); + System.out.println(result.getOutput()); + // → "2 + 2 equals 4." +} +``` + +## Add a tool + +Tools are Java methods wrapped as Conductor worker tasks. The method runs locally in your process; the agent calls it remotely via Conductor. + +```java +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.annotations.Tool; + +public class WeatherTools { + + @Tool(name = "get_weather", description = "Get current weather for a city") + public String getWeather(String city) { + // real implementation would call a weather API + return "Sunny, 22°C in " + city; + } +} + +Agent agent = Agent.builder() + .name("weather_agent") + .model("openai/gpt-4o-mini") + .instructions("Answer weather questions using the get_weather tool.") + .tools(ToolRegistry.fromInstance(new WeatherTools())) + .build(); + +try (AgentRuntime runtime = new AgentRuntime()) { + AgentResult result = runtime.run(agent, "What's the weather in Tokyo?"); + System.out.println(result.getOutput()); +} +``` + +!!! tip "Tool methods run as Conductor tasks" + The `@Tool` method executes in your local JVM, but Conductor manages its lifecycle. If your process restarts mid-run, Conductor re-dispatches the task to the next available worker. + +## Streaming + +Use `stream()` to get events as they happen: + +```java +import org.conductoross.conductor.ai.model.AgentStream; +import org.conductoross.conductor.ai.model.AgentEvent; + +try (AgentRuntime runtime = new AgentRuntime(); + AgentStream stream = runtime.stream(agent, "Tell me a story")) { + + for (AgentEvent event : stream) { + if (event.getType().isMessage()) { + System.out.print(event.getContent()); + } + } +} +``` + +## Next steps + +- [Concepts → Agents](concepts/agents.md) — full builder API reference +- [Concepts → Tools](concepts/tools.md) — tool types: HTTP, MCP, human, CLI +- [Concepts → Multi-Agent](concepts/multi-agent.md) — sequential, parallel, handoff, swarm +- [Spring Boot](spring-boot.md) — auto-configuration for Spring Boot apps diff --git a/sdk/java/docs/index.md b/sdk/java/docs/index.md new file mode 100644 index 000000000..43cbaa249 --- /dev/null +++ b/sdk/java/docs/index.md @@ -0,0 +1,59 @@ +# Agentspan Java SDK + +Build durable AI agents in Java, backed by [Conductor](https://conductor.netflix.com/) workflows. Your agents survive process crashes, tool calls scale independently, and human approvals can take days — all without managing state yourself. + +## Installation + +=== "Gradle" + + ```groovy + implementation 'org.conductoross.conductor:conductor-ai-sdk:0.1.0' + ``` + +=== "Maven" + + ```xml + + org.conductoross.conductor + conductor-ai-sdk + 0.1.0 + + ``` + +**Requirements:** Java 21+ · Agentspan server (see [self-hosting](../self-hosting.md)) + +## Hello World + +```java +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; + +Agent agent = Agent.builder() + .name("assistant") + .model("openai/gpt-4o-mini") + .instructions("You are a helpful assistant.") + .build(); + +try (AgentRuntime runtime = new AgentRuntime()) { + AgentResult result = runtime.run(agent, "What is the capital of France?"); + System.out.println(result.getOutput()); +} +``` + +## What makes it different + +| Feature | Agentspan | Thread-based SDKs | +|---|---|---| +| Survives crashes | ✅ Conductor workflow | ❌ State lost | +| Tool workers | ✅ Distributed tasks | ❌ In-process only | +| Long-running | ✅ Days / weeks | ❌ Minutes | +| Human-in-the-loop | ✅ Native approval flow | ❌ Polling hacks | +| Observability | ✅ Full workflow audit log | ❌ Log scraping | + +## Next steps + +- [Getting Started](getting-started.md) — install, configure, and run your first agent +- [Core Concepts → Agents](concepts/agents.md) — the full `Agent.builder()` API +- [Core Concepts → Tools](concepts/tools.md) — Java methods as Conductor worker tasks +- [API Reference](api-reference.md) — complete method signatures diff --git a/sdk/java/docs/mkdocs.yml b/sdk/java/docs/mkdocs.yml new file mode 100644 index 000000000..2abf34915 --- /dev/null +++ b/sdk/java/docs/mkdocs.yml @@ -0,0 +1,59 @@ +site_name: Agentspan Java SDK +site_description: Build durable AI agents in Java with Agentspan. +site_url: https://agentspan.ai/docs/java-sdk/ +repo_url: https://github.com/agentspan-ai/agentspan +repo_name: agentspan-ai/agentspan +edit_uri: edit/main/sdk/java/docs/ + +docs_dir: . +site_dir: ../../build/site/java-sdk +strict: false + +theme: + name: material + features: + - navigation.instant + - navigation.sections + - navigation.top + - content.code.copy + - content.action.edit + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + +nav: + - Overview: index.md + - Getting Started: getting-started.md + - Core Concepts: + - Agents: concepts/agents.md + - Agent Field Reference: agent-structure.md + - Agent JSON Schema: agent-schema.md + - Tools: concepts/tools.md + - Multi-Agent: concepts/multi-agent.md + - Guardrails: concepts/guardrails.md + - Termination: concepts/termination.md + - Scheduling: concepts/scheduling.md + - Skills: concepts/skills.md + - Frameworks: + - LangChain4j: frameworks/langchain4j.md + - OpenAI Agents SDK: frameworks/openai.md + - Google ADK: frameworks/google-adk.md + - Spring Boot: spring-boot.md + - API Reference: + - AgentRuntime: agent-runtime-api.md + - AgentClient (internal): agent-client-api.md + - Public API summary: api-reference.md diff --git a/sdk/java/docs/spring-boot.md b/sdk/java/docs/spring-boot.md new file mode 100644 index 000000000..d529b1a80 --- /dev/null +++ b/sdk/java/docs/spring-boot.md @@ -0,0 +1,144 @@ +# Spring Boot + +The `conductor-ai-sdk-spring` module provides Spring Boot auto-configuration. Add it and your `AgentRuntime` is wired automatically from `application.properties`. + +## Dependency + +=== "Gradle" + + ```groovy + implementation 'org.conductoross.conductor:conductor-ai-sdk-spring:0.1.0' + ``` + +=== "Maven" + + ```xml + + org.conductoross.conductor + conductor-ai-sdk-spring + 0.1.0 + + ``` + +This pulls in both `conductor-ai-sdk` and `conductor-client-spring` (which wires the `ApiClient`). + +## Configuration + +```properties +# application.properties + +# Conductor server — from conductor-client-spring +conductor.root-uri=http://localhost:6767/api +conductor.security.client.key-id=your-key # optional +conductor.security.client.secret=your-secret # optional + +# Agentspan worker tuning +agentspan.worker-poll-interval-ms=100 +agentspan.worker-thread-count=1 +``` + +## Inject and use + +```java +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.Agent; +import org.springframework.stereotype.Service; + +@Service +public class ChatService { + + private final AgentRuntime runtime; + + public ChatService(AgentRuntime runtime) { + this.runtime = runtime; + } + + public String answer(String question) { + Agent agent = Agent.builder() + .name("assistant") + .model("openai/gpt-4o-mini") + .instructions("You are a helpful assistant.") + .build(); + + return runtime.run(agent, question).getOutput(); + } +} +``` + +## Declare agents on beans with @AgentDef + +Any Spring bean can declare agents with [`@AgentDef` methods](concepts/agents.md#agentdef-annotation). The auto-configured `AgentCatalog` collects them from every bean in the context: + +```java +import org.conductoross.conductor.ai.annotations.AgentDef; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.spring.AgentCatalog; + +@Component +public class SupportCrew { + + @Tool(description = "Look up an order by id") + public String lookupOrder(String orderId) { ... } + + @AgentDef(model = "openai/gpt-4o") // lookupOrder attaches automatically + public String support() { + return "You handle support tickets."; + } +} + +@Service +public class TicketService { + private final AgentRuntime runtime; + private final AgentCatalog agents; + + public TicketService(AgentRuntime runtime, AgentCatalog agents) { + this.runtime = runtime; + this.agents = agents; + } + + public String answer(String ticket) { + return runtime.run(agents.get("support"), ticket).getOutput().toString(); + } +} +``` + +The catalog scans lazily on first access; only beans whose class declares `@AgentDef` methods are touched. Duplicate agent names across beans fail fast with both bean names in the error. Proxied beans (e.g. `@Transactional`) work — discovery looks through the proxy subclass to the annotated declaration, and invocation goes through the proxy. + +## Beans provided + +| Bean type | Bean name | Condition | +|---|---|---| +| `ApiClient` | `orkesConductorClient` | From `conductor-client-spring`; `@ConditionalOnMissingBean` | +| `AgentConfig` | `agentspanConfig` | From `agentspan.*` properties; `@ConditionalOnMissingBean` | +| `AgentRuntime` | `agentRuntime` | Wires `ApiClient` + `AgentConfig`; `@ConditionalOnMissingBean` | +| `AgentCatalog` | `agentCatalog` | Collects `@AgentDef` agents from all beans; `@ConditionalOnMissingBean` | + +All beans are `@ConditionalOnMissingBean` — declare your own to override any of them. + +## Override the ApiClient + +To connect to multiple servers or use custom TLS: + +```java +@Configuration +public class MyAgentspanConfig { + + @Bean + public ApiClient agentspanClient() { + return AgentRuntime.client("http://myserver:6767", "key", "secret"); + } +} +``` + +## Override AgentConfig + +```java +@Bean +public AgentConfig agentspanConfig() { + return new AgentConfig(500, 4); // 500ms poll, 4 worker threads +} +``` + +## Graceful shutdown + +`AgentRuntime` is `AutoCloseable`. Spring calls `close()` on context shutdown automatically when it is a Spring-managed bean — worker threads stop and HTTP connections are released cleanly. diff --git a/sdk/java/e2e/BaseTest.java b/sdk/java/e2e/BaseTest.java index 4065bf162..bd38b051f 100644 --- a/sdk/java/e2e/BaseTest.java +++ b/sdk/java/e2e/BaseTest.java @@ -1,7 +1,8 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -import org.junit.jupiter.api.BeforeAll; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.net.URI; import java.net.http.HttpClient; @@ -11,10 +12,10 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.junit.jupiter.api.BeforeAll; -import static org.junit.jupiter.api.Assertions.fail; -import static org.junit.jupiter.api.Assumptions.assumeTrue; +import com.fasterxml.jackson.databind.ObjectMapper; /** * Base class for all e2e tests. @@ -30,18 +31,16 @@ public abstract class BaseTest { /** API URL for the Agentspan server (includes /api suffix). */ protected static final String SERVER_URL = - System.getenv().getOrDefault("AGENTSPAN_SERVER_URL", "http://localhost:6767/api"); + System.getenv().getOrDefault("AGENTSPAN_SERVER_URL", "http://localhost:6767/api"); /** Base URL (without /api) for health checks and workflow fetches. */ protected static final String BASE_URL = SERVER_URL.replace("/api", ""); /** LLM model to use in e2e tests. */ - protected static final String MODEL = - System.getenv().getOrDefault("AGENTSPAN_LLM_MODEL", "openai/gpt-4o-mini"); + protected static final String MODEL = System.getenv().getOrDefault("AGENTSPAN_LLM_MODEL", "openai/gpt-4o-mini"); - private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)) - .build(); + private static final HttpClient HTTP_CLIENT = + HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -53,12 +52,11 @@ public abstract class BaseTest { static void checkServerHealth() { try { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(BASE_URL + "/health")) - .timeout(Duration.ofSeconds(5)) - .GET() - .build(); - HttpResponse response = HTTP_CLIENT.send(request, - HttpResponse.BodyHandlers.ofString()); + .uri(URI.create(BASE_URL + "/health")) + .timeout(Duration.ofSeconds(5)) + .GET() + .build(); + HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); boolean healthy = false; if (response.statusCode() == 200 && response.body() != null) { @@ -67,11 +65,9 @@ static void checkServerHealth() { Object h = body.get("healthy"); healthy = Boolean.TRUE.equals(h); } - assumeTrue(healthy, - "Server at " + BASE_URL + " is not healthy — skipping e2e tests"); + assumeTrue(healthy, "Server at " + BASE_URL + " is not healthy — skipping e2e tests"); } catch (Exception e) { - assumeTrue(false, - "Server not available at " + BASE_URL + ": " + e.getMessage()); + assumeTrue(false, "Server not available at " + BASE_URL + ": " + e.getMessage()); } } @@ -85,12 +81,11 @@ static void checkServerHealth() { protected Map getWorkflow(String executionId) { try { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(BASE_URL + "/api/workflow/" + executionId)) - .timeout(Duration.ofSeconds(10)) - .GET() - .build(); - HttpResponse response = HTTP_CLIENT.send(request, - HttpResponse.BodyHandlers.ofString()); + .uri(URI.create(BASE_URL + "/api/workflow/" + executionId)) + .timeout(Duration.ofSeconds(10)) + .GET() + .build(); + HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { fail("Failed to fetch workflow " + executionId + ": HTTP " + response.statusCode()); } @@ -111,12 +106,11 @@ protected Map getWorkflow(String executionId) { * @return the agentDef map */ @SuppressWarnings("unchecked") - protected Map getAgentDef(Map plan) { - Object wfObj = plan.get("workflowDef"); - if (wfObj == null) { - fail("plan() result missing 'workflowDef'. Top-level keys: " + plan.keySet()); + protected Map getAgentDef(CompileResponse plan) { + Map wf = plan.getWorkflowDef(); + if (wf == null || wf.isEmpty()) { + fail("plan() result has no workflowDef"); } - Map wf = (Map) wfObj; Object metaObj = wf.get("metadata"); if (metaObj == null) { diff --git a/sdk/java/e2e/PlanExecuteTest.java b/sdk/java/e2e/PlanExecuteTest.java index 774479600..6abba22bd 100644 --- a/sdk/java/e2e/PlanExecuteTest.java +++ b/sdk/java/e2e/PlanExecuteTest.java @@ -1,19 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentConfig; -import ai.agentspan.AgentRuntime; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; -import ai.agentspan.plans.Op; -import ai.agentspan.plans.Plan; -import ai.agentspan.plans.Ref; -import ai.agentspan.plans.Step; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.io.File; import java.io.IOException; @@ -25,7 +13,18 @@ import java.util.Map; import java.util.concurrent.TimeUnit; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.plans.Op; +import org.conductoross.conductor.ai.plans.Plan; +import org.conductoross.conductor.ai.plans.Ref; +import org.conductoross.conductor.ai.plans.Step; +import org.junit.jupiter.api.*; /** * Plan-Execute strategy e2e test — runs real agents with real LLM calls. @@ -54,7 +53,7 @@ class PlanExecuteTest extends BaseTest { @BeforeAll static void setUp() { - runtime = new AgentRuntime(new AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -66,9 +65,9 @@ static void tearDown() { void cleanWorkDir() throws IOException { if (Files.exists(WORK_DIR)) { Files.walk(WORK_DIR) - .sorted(Comparator.reverseOrder()) - .map(Path::toFile) - .forEach(File::delete); + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); } Files.createDirectories(WORK_DIR); } @@ -77,7 +76,8 @@ void cleanWorkDir() throws IOException { static ToolDef createDirectoryTool() { Map props = new LinkedHashMap<>(); - props.put("path", Map.of("type", "string", "description", "Directory path to create (relative to working dir).")); + props.put( + "path", Map.of("type", "string", "description", "Directory path to create (relative to working dir).")); Map inputSchema = new LinkedHashMap<>(); inputSchema.put("type", "object"); @@ -85,21 +85,21 @@ static ToolDef createDirectoryTool() { inputSchema.put("required", List.of("path")); return ToolDef.builder() - .name("create_directory") - .description("Create a directory (and parents) if it doesn't exist.") - .inputSchema(inputSchema) - .toolType("worker") - .func(input -> { - String path = (String) input.get("path"); - Path full = WORK_DIR.resolve(path); - try { - Files.createDirectories(full); - } catch (IOException e) { - return "ERROR: " + e.getMessage(); - } - return "Created directory: " + full; - }) - .build(); + .name("create_directory") + .description("Create a directory (and parents) if it doesn't exist.") + .inputSchema(inputSchema) + .toolType("worker") + .func(input -> { + String path = (String) input.get("path"); + Path full = WORK_DIR.resolve(path); + try { + Files.createDirectories(full); + } catch (IOException e) { + return "ERROR: " + e.getMessage(); + } + return "Created directory: " + full; + }) + .build(); } static ToolDef writeFileTool() { @@ -113,23 +113,23 @@ static ToolDef writeFileTool() { inputSchema.put("required", List.of("path", "content")); return ToolDef.builder() - .name("write_file") - .description("Write content to a file, creating parent directories if needed.") - .inputSchema(inputSchema) - .toolType("worker") - .func(input -> { - String path = (String) input.get("path"); - String content = (String) input.get("content"); - Path full = WORK_DIR.resolve(path); - try { - Files.createDirectories(full.getParent()); - Files.writeString(full, content); - } catch (IOException e) { - return "ERROR: " + e.getMessage(); - } - return "Wrote " + content.length() + " bytes to " + full; - }) - .build(); + .name("write_file") + .description("Write content to a file, creating parent directories if needed.") + .inputSchema(inputSchema) + .toolType("worker") + .func(input -> { + String path = (String) input.get("path"); + String content = (String) input.get("content"); + Path full = WORK_DIR.resolve(path); + try { + Files.createDirectories(full.getParent()); + Files.writeString(full, content); + } catch (IOException e) { + return "ERROR: " + e.getMessage(); + } + return "Wrote " + content.length() + " bytes to " + full; + }) + .build(); } static ToolDef readFileTool() { @@ -142,29 +142,32 @@ static ToolDef readFileTool() { inputSchema.put("required", List.of("path")); return ToolDef.builder() - .name("read_file") - .description("Read the contents of a file.") - .inputSchema(inputSchema) - .toolType("worker") - .func(input -> { - String path = (String) input.get("path"); - Path full = WORK_DIR.resolve(path); - if (!Files.exists(full)) { - return "ERROR: File not found: " + full; - } - try { - return Files.readString(full); - } catch (IOException e) { - return "ERROR: " + e.getMessage(); - } - }) - .build(); + .name("read_file") + .description("Read the contents of a file.") + .inputSchema(inputSchema) + .toolType("worker") + .func(input -> { + String path = (String) input.get("path"); + Path full = WORK_DIR.resolve(path); + if (!Files.exists(full)) { + return "ERROR: File not found: " + full; + } + try { + return Files.readString(full); + } catch (IOException e) { + return "ERROR: " + e.getMessage(); + } + }) + .build(); } static ToolDef assembleFilesTool() { Map props = new LinkedHashMap<>(); - props.put("output_path", Map.of("type", "string", "description", "Output file path (relative to working dir).")); - props.put("input_paths", Map.of("type", "string", "description", "JSON array of input file paths (relative to working dir).")); + props.put( + "output_path", Map.of("type", "string", "description", "Output file path (relative to working dir).")); + props.put( + "input_paths", + Map.of("type", "string", "description", "JSON array of input file paths (relative to working dir).")); props.put("separator", Map.of("type", "string", "description", "Text to insert between file contents.")); Map inputSchema = new LinkedHashMap<>(); @@ -173,52 +176,55 @@ static ToolDef assembleFilesTool() { inputSchema.put("required", List.of("output_path", "input_paths")); return ToolDef.builder() - .name("assemble_files") - .description("Concatenate multiple files into one, with a separator between them.") - .inputSchema(inputSchema) - .toolType("worker") - .func(input -> { - String outputPath = (String) input.get("output_path"); - String inputPathsJson = (String) input.get("input_paths"); - String separator = input.get("separator") instanceof String - ? (String) input.get("separator") : "\n\n---\n\n"; - - List paths; - try { - com.fasterxml.jackson.databind.ObjectMapper mapper = - new com.fasterxml.jackson.databind.ObjectMapper(); - paths = mapper.readValue(inputPathsJson, - mapper.getTypeFactory().constructCollectionType(List.class, String.class)); - } catch (Exception e) { - return "ERROR: Failed to parse input_paths: " + e.getMessage(); - } + .name("assemble_files") + .description("Concatenate multiple files into one, with a separator between them.") + .inputSchema(inputSchema) + .toolType("worker") + .func(input -> { + String outputPath = (String) input.get("output_path"); + String inputPathsJson = (String) input.get("input_paths"); + String separator = + input.get("separator") instanceof String ? (String) input.get("separator") : "\n\n---\n\n"; + + List paths; + try { + com.fasterxml.jackson.databind.ObjectMapper mapper = + new com.fasterxml.jackson.databind.ObjectMapper(); + paths = mapper.readValue( + inputPathsJson, + mapper.getTypeFactory().constructCollectionType(List.class, String.class)); + } catch (Exception e) { + return "ERROR: Failed to parse input_paths: " + e.getMessage(); + } - StringBuilder combined = new StringBuilder(); - for (int i = 0; i < paths.size(); i++) { - if (i > 0) combined.append(separator); - Path full = WORK_DIR.resolve(paths.get(i)); - if (Files.exists(full)) { - try { - combined.append(Files.readString(full)); - } catch (IOException e) { - combined.append("[Error reading: ").append(paths.get(i)).append("]"); + StringBuilder combined = new StringBuilder(); + for (int i = 0; i < paths.size(); i++) { + if (i > 0) combined.append(separator); + Path full = WORK_DIR.resolve(paths.get(i)); + if (Files.exists(full)) { + try { + combined.append(Files.readString(full)); + } catch (IOException e) { + combined.append("[Error reading: ") + .append(paths.get(i)) + .append("]"); + } + } else { + combined.append("[Missing: ").append(paths.get(i)).append("]"); } - } else { - combined.append("[Missing: ").append(paths.get(i)).append("]"); } - } - Path outFull = WORK_DIR.resolve(outputPath); - try { - Files.createDirectories(outFull.getParent()); - Files.writeString(outFull, combined.toString()); - } catch (IOException e) { - return "ERROR: " + e.getMessage(); - } - return "Assembled " + paths.size() + " files into " + outFull - + " (" + combined.length() + " bytes)"; - }) - .build(); + Path outFull = WORK_DIR.resolve(outputPath); + try { + Files.createDirectories(outFull.getParent()); + Files.writeString(outFull, combined.toString()); + } catch (IOException e) { + return "ERROR: " + e.getMessage(); + } + return "Assembled " + paths.size() + " files into " + outFull + " (" + combined.length() + + " bytes)"; + }) + .build(); } static ToolDef checkWordCountTool() { @@ -232,219 +238,220 @@ static ToolDef checkWordCountTool() { inputSchema.put("required", List.of("path", "min_words")); return ToolDef.builder() - .name("check_word_count") - .description("Check that a file meets a minimum word count.") - .inputSchema(inputSchema) - .toolType("worker") - .func(input -> { - String path = (String) input.get("path"); - Object minWordsRaw = input.get("min_words"); - int minWords = minWordsRaw instanceof Number - ? ((Number) minWordsRaw).intValue() : 200; - - Path full = WORK_DIR.resolve(path); - if (!Files.exists(full)) { - return "{\"passed\": false, \"error\": \"File not found: " + path - + "\", \"word_count\": 0}"; - } - String content; - try { - content = Files.readString(full); - } catch (IOException e) { - return "{\"passed\": false, \"error\": \"" + e.getMessage() - + "\", \"word_count\": 0}"; - } - int count = content.split("\\s+").length; - boolean passed = count >= minWords; - return "{\"passed\": " + passed + ", \"word_count\": " + count - + ", \"min_words\": " + minWords + "}"; - }) - .build(); + .name("check_word_count") + .description("Check that a file meets a minimum word count.") + .inputSchema(inputSchema) + .toolType("worker") + .func(input -> { + String path = (String) input.get("path"); + Object minWordsRaw = input.get("min_words"); + int minWords = minWordsRaw instanceof Number ? ((Number) minWordsRaw).intValue() : 200; + + Path full = WORK_DIR.resolve(path); + if (!Files.exists(full)) { + return "{\"passed\": false, \"error\": \"File not found: " + path + "\", \"word_count\": 0}"; + } + String content; + try { + content = Files.readString(full); + } catch (IOException e) { + return "{\"passed\": false, \"error\": \"" + e.getMessage() + "\", \"word_count\": 0}"; + } + int count = content.split("\\s+").length; + boolean passed = count >= minWords; + return "{\"passed\": " + passed + ", \"word_count\": " + count + ", \"min_words\": " + minWords + + "}"; + }) + .build(); } // ── Agent instructions (max_tokens variant) ───────────────────────── - static final String MAX_TOKENS_PLANNER_INSTRUCTIONS = "You are a research report planner. Given a topic, plan a detailed report.\n" - + "\n" - + "Your job:\n" - + "1. Decide on 3 sections for the report (introduction, body, conclusion)\n" - + "2. For each section, write clear instructions requesting DETAILED content (250+ words each)\n" - + "3. Output your plan as Markdown with an embedded JSON fence\n" - + "\n" - + "IMPORTANT: Your plan MUST include a ```json fence with the structured plan.\n" - + "IMPORTANT: Every generate block MUST include \"max_tokens\": 8192.\n" - + "\n" - + "## Available tools:\n" - + "- `create_directory`: args={path}\n" - + "- `write_file`: generate={instructions, output_schema, max_tokens}\n" - + "- `assemble_files`: args={output_path, input_paths, separator}\n" - + "- `check_word_count`: args={path, min_words}\n" - + "\n" - + "## Plan format:\n" - + "\n" - + "```json\n" - + "{\n" - + " \"steps\": [\n" - + " {\n" - + " \"id\": \"setup\",\n" - + " \"parallel\": false,\n" - + " \"operations\": [\n" - + " {\"tool\": \"create_directory\", \"args\": {\"path\": \"sections\"}}\n" - + " ]\n" - + " },\n" - + " {\n" - + " \"id\": \"write_sections\",\n" - + " \"depends_on\": [\"setup\"],\n" - + " \"parallel\": true,\n" - + " \"operations\": [\n" - + " {\n" - + " \"tool\": \"write_file\",\n" - + " \"generate\": {\n" - + " \"instructions\": \"Write a detailed 250+ word introduction about [topic].\",\n" - + " \"output_schema\": \"{\\\"path\\\": \\\"sections/01_intro.md\\\", \\\"content\\\": \\\"...\\\"}\",\n" - + " \"max_tokens\": 8192\n" - + " }\n" - + " },\n" - + " {\n" - + " \"tool\": \"write_file\",\n" - + " \"generate\": {\n" - + " \"instructions\": \"Write a detailed 250+ word body section about [subtopic].\",\n" - + " \"output_schema\": \"{\\\"path\\\": \\\"sections/02_body.md\\\", \\\"content\\\": \\\"...\\\"}\",\n" - + " \"max_tokens\": 8192\n" - + " }\n" - + " },\n" - + " {\n" - + " \"tool\": \"write_file\",\n" - + " \"generate\": {\n" - + " \"instructions\": \"Write a detailed 250+ word conclusion about [topic].\",\n" - + " \"output_schema\": \"{\\\"path\\\": \\\"sections/03_conclusion.md\\\", \\\"content\\\": \\\"...\\\"}\",\n" - + " \"max_tokens\": 8192\n" - + " }\n" - + " }\n" - + " ]\n" - + " },\n" - + " {\n" - + " \"id\": \"assemble\",\n" - + " \"depends_on\": [\"write_sections\"],\n" - + " \"parallel\": false,\n" - + " \"operations\": [\n" - + " {\n" - + " \"tool\": \"assemble_files\",\n" - + " \"args\": {\n" - + " \"output_path\": \"report.md\",\n" - + " \"input_paths\": \"[\\\"sections/01_intro.md\\\", \\\"sections/02_body.md\\\", \\\"sections/03_conclusion.md\\\"]\",\n" - + " \"separator\": \"\\n\\n---\\n\\n\"\n" - + " }\n" - + " }\n" - + " ]\n" - + " }\n" - + " ],\n" - + " \"validation\": [\n" - + " {\"tool\": \"check_word_count\", \"args\": {\"path\": \"report.md\", \"min_words\": " + MIN_WORD_COUNT + "}}\n" - + " ],\n" - + " \"on_success\": []\n" - + "}\n" - + "```\n" - + "\n" - + "## Rules:\n" - + "- Section files go in sections/ directory\n" - + "- Each section MUST be 250+ words (detailed, thorough)\n" - + "- Every generate block MUST include \"max_tokens\": 8192\n" - + "- The assemble step must list ALL section files in order\n" - + "- Always validate with check_word_count (min " + MIN_WORD_COUNT + " words)\n" - + "- The JSON must be valid\n"; + static final String MAX_TOKENS_PLANNER_INSTRUCTIONS = + "You are a research report planner. Given a topic, plan a detailed report.\n" + + "\n" + + "Your job:\n" + + "1. Decide on 3 sections for the report (introduction, body, conclusion)\n" + + "2. For each section, write clear instructions requesting DETAILED content (250+ words each)\n" + + "3. Output your plan as Markdown with an embedded JSON fence\n" + + "\n" + + "IMPORTANT: Your plan MUST include a ```json fence with the structured plan.\n" + + "IMPORTANT: Every generate block MUST include \"max_tokens\": 8192.\n" + + "\n" + + "## Available tools:\n" + + "- `create_directory`: args={path}\n" + + "- `write_file`: generate={instructions, output_schema, max_tokens}\n" + + "- `assemble_files`: args={output_path, input_paths, separator}\n" + + "- `check_word_count`: args={path, min_words}\n" + + "\n" + + "## Plan format:\n" + + "\n" + + "```json\n" + + "{\n" + + " \"steps\": [\n" + + " {\n" + + " \"id\": \"setup\",\n" + + " \"parallel\": false,\n" + + " \"operations\": [\n" + + " {\"tool\": \"create_directory\", \"args\": {\"path\": \"sections\"}}\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"id\": \"write_sections\",\n" + + " \"depends_on\": [\"setup\"],\n" + + " \"parallel\": true,\n" + + " \"operations\": [\n" + + " {\n" + + " \"tool\": \"write_file\",\n" + + " \"generate\": {\n" + + " \"instructions\": \"Write a detailed 250+ word introduction about [topic].\",\n" + + " \"output_schema\": \"{\\\"path\\\": \\\"sections/01_intro.md\\\", \\\"content\\\": \\\"...\\\"}\",\n" + + " \"max_tokens\": 8192\n" + + " }\n" + + " },\n" + + " {\n" + + " \"tool\": \"write_file\",\n" + + " \"generate\": {\n" + + " \"instructions\": \"Write a detailed 250+ word body section about [subtopic].\",\n" + + " \"output_schema\": \"{\\\"path\\\": \\\"sections/02_body.md\\\", \\\"content\\\": \\\"...\\\"}\",\n" + + " \"max_tokens\": 8192\n" + + " }\n" + + " },\n" + + " {\n" + + " \"tool\": \"write_file\",\n" + + " \"generate\": {\n" + + " \"instructions\": \"Write a detailed 250+ word conclusion about [topic].\",\n" + + " \"output_schema\": \"{\\\"path\\\": \\\"sections/03_conclusion.md\\\", \\\"content\\\": \\\"...\\\"}\",\n" + + " \"max_tokens\": 8192\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"id\": \"assemble\",\n" + + " \"depends_on\": [\"write_sections\"],\n" + + " \"parallel\": false,\n" + + " \"operations\": [\n" + + " {\n" + + " \"tool\": \"assemble_files\",\n" + + " \"args\": {\n" + + " \"output_path\": \"report.md\",\n" + + " \"input_paths\": \"[\\\"sections/01_intro.md\\\", \\\"sections/02_body.md\\\", \\\"sections/03_conclusion.md\\\"]\",\n" + + " \"separator\": \"\\n\\n---\\n\\n\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"validation\": [\n" + + " {\"tool\": \"check_word_count\", \"args\": {\"path\": \"report.md\", \"min_words\": " + + MIN_WORD_COUNT + "}}\n" + + " ],\n" + + " \"on_success\": []\n" + + "}\n" + + "```\n" + + "\n" + + "## Rules:\n" + + "- Section files go in sections/ directory\n" + + "- Each section MUST be 250+ words (detailed, thorough)\n" + + "- Every generate block MUST include \"max_tokens\": 8192\n" + + "- The assemble step must list ALL section files in order\n" + + "- Always validate with check_word_count (min " + MIN_WORD_COUNT + " words)\n" + + "- The JSON must be valid\n"; // ── Agent instructions ─────────────────────────────────────────────── - static final String PLANNER_INSTRUCTIONS = "You are a research report planner. Given a topic, plan a structured report.\n" - + "\n" - + "Your job:\n" - + "1. Decide on 3 sections for the report (introduction, body, conclusion)\n" - + "2. For each section, write clear instructions on what content to include\n" - + "3. Output your plan as Markdown with an embedded JSON fence\n" - + "\n" - + "IMPORTANT: Your plan MUST include a ```json fence with the structured plan.\n" - + "\n" - + "## Available tools for operations:\n" - + "- `create_directory`: args={path} — create a directory\n" - + "- `write_file`: generate={instructions, output_schema} — LLM writes content\n" - + "- `assemble_files`: args={output_path, input_paths, separator} — concatenate files\n" - + "- `check_word_count`: args={path, min_words} — validate word count\n" - + "\n" - + "## Plan format:\n" - + "\n" - + "Your output MUST end with a JSON fence like this example:\n" - + "\n" - + "```json\n" - + "{\n" - + " \"steps\": [\n" - + " {\n" - + " \"id\": \"setup\",\n" - + " \"parallel\": false,\n" - + " \"operations\": [\n" - + " {\"tool\": \"create_directory\", \"args\": {\"path\": \"sections\"}}\n" - + " ]\n" - + " },\n" - + " {\n" - + " \"id\": \"write_sections\",\n" - + " \"depends_on\": [\"setup\"],\n" - + " \"parallel\": true,\n" - + " \"operations\": [\n" - + " {\n" - + " \"tool\": \"write_file\",\n" - + " \"generate\": {\n" - + " \"instructions\": \"Write a 100-word introduction about [topic].\",\n" - + " \"output_schema\": \"{\\\"path\\\": \\\"sections/01_intro.md\\\", \\\"content\\\": \\\"...\\\"}\"\n" - + " }\n" - + " },\n" - + " {\n" - + " \"tool\": \"write_file\",\n" - + " \"generate\": {\n" - + " \"instructions\": \"Write a 100-word section about [subtopic].\",\n" - + " \"output_schema\": \"{\\\"path\\\": \\\"sections/02_body.md\\\", \\\"content\\\": \\\"...\\\"}\"\n" - + " }\n" - + " }\n" - + " ]\n" - + " },\n" - + " {\n" - + " \"id\": \"assemble\",\n" - + " \"depends_on\": [\"write_sections\"],\n" - + " \"parallel\": false,\n" - + " \"operations\": [\n" - + " {\n" - + " \"tool\": \"assemble_files\",\n" - + " \"args\": {\n" - + " \"output_path\": \"report.md\",\n" - + " \"input_paths\": \"[\\\"sections/01_intro.md\\\", \\\"sections/02_body.md\\\"]\",\n" - + " \"separator\": \"\\n\\n---\\n\\n\"\n" - + " }\n" - + " }\n" - + " ]\n" - + " }\n" - + " ],\n" - + " \"validation\": [\n" - + " {\"tool\": \"check_word_count\", \"args\": {\"path\": \"report.md\", \"min_words\": " + MIN_WORD_COUNT + "}}\n" - + " ],\n" - + " \"on_success\": []\n" - + "}\n" - + "```\n" - + "\n" - + "## Rules:\n" - + "- Section files go in sections/ directory (01_intro.md, 02_body.md, etc.)\n" - + "- Each section should be 80-150 words\n" - + "- The assemble step must list ALL section files in order\n" - + "- Always validate with check_word_count (min " + MIN_WORD_COUNT + " words)\n" - + "- Keep it simple: 3 sections total\n" - + "- The JSON must be valid\n"; + static final String PLANNER_INSTRUCTIONS = + "You are a research report planner. Given a topic, plan a structured report.\n" + + "\n" + + "Your job:\n" + + "1. Decide on 3 sections for the report (introduction, body, conclusion)\n" + + "2. For each section, write clear instructions on what content to include\n" + + "3. Output your plan as Markdown with an embedded JSON fence\n" + + "\n" + + "IMPORTANT: Your plan MUST include a ```json fence with the structured plan.\n" + + "\n" + + "## Available tools for operations:\n" + + "- `create_directory`: args={path} — create a directory\n" + + "- `write_file`: generate={instructions, output_schema} — LLM writes content\n" + + "- `assemble_files`: args={output_path, input_paths, separator} — concatenate files\n" + + "- `check_word_count`: args={path, min_words} — validate word count\n" + + "\n" + + "## Plan format:\n" + + "\n" + + "Your output MUST end with a JSON fence like this example:\n" + + "\n" + + "```json\n" + + "{\n" + + " \"steps\": [\n" + + " {\n" + + " \"id\": \"setup\",\n" + + " \"parallel\": false,\n" + + " \"operations\": [\n" + + " {\"tool\": \"create_directory\", \"args\": {\"path\": \"sections\"}}\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"id\": \"write_sections\",\n" + + " \"depends_on\": [\"setup\"],\n" + + " \"parallel\": true,\n" + + " \"operations\": [\n" + + " {\n" + + " \"tool\": \"write_file\",\n" + + " \"generate\": {\n" + + " \"instructions\": \"Write a 100-word introduction about [topic].\",\n" + + " \"output_schema\": \"{\\\"path\\\": \\\"sections/01_intro.md\\\", \\\"content\\\": \\\"...\\\"}\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"tool\": \"write_file\",\n" + + " \"generate\": {\n" + + " \"instructions\": \"Write a 100-word section about [subtopic].\",\n" + + " \"output_schema\": \"{\\\"path\\\": \\\"sections/02_body.md\\\", \\\"content\\\": \\\"...\\\"}\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"id\": \"assemble\",\n" + + " \"depends_on\": [\"write_sections\"],\n" + + " \"parallel\": false,\n" + + " \"operations\": [\n" + + " {\n" + + " \"tool\": \"assemble_files\",\n" + + " \"args\": {\n" + + " \"output_path\": \"report.md\",\n" + + " \"input_paths\": \"[\\\"sections/01_intro.md\\\", \\\"sections/02_body.md\\\"]\",\n" + + " \"separator\": \"\\n\\n---\\n\\n\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"validation\": [\n" + + " {\"tool\": \"check_word_count\", \"args\": {\"path\": \"report.md\", \"min_words\": " + + MIN_WORD_COUNT + "}}\n" + + " ],\n" + + " \"on_success\": []\n" + + "}\n" + + "```\n" + + "\n" + + "## Rules:\n" + + "- Section files go in sections/ directory (01_intro.md, 02_body.md, etc.)\n" + + "- Each section should be 80-150 words\n" + + "- The assemble step must list ALL section files in order\n" + + "- Always validate with check_word_count (min " + MIN_WORD_COUNT + " words)\n" + + "- Keep it simple: 3 sections total\n" + + "- The JSON must be valid\n"; static final String FALLBACK_INSTRUCTIONS = "You are fixing a report that failed validation. " - + "The plan was already partially executed but something went wrong " - + "(missing sections, word count too low, etc.).\n" - + "\n" - + "Review the error output, figure out what's missing or broken, and fix it.\n" - + "You have access to read_file, write_file, assemble_files, and check_word_count.\n" - + "\n" - + "Working directory: " + WORK_DIR; + + "The plan was already partially executed but something went wrong " + + "(missing sections, word count too low, etc.).\n" + + "\n" + + "Review the error output, figure out what's missing or broken, and fix it.\n" + + "You have access to read_file, write_file, assemble_files, and check_word_count.\n" + + "\n" + + "Working directory: " + WORK_DIR; // ── Tests ──────────────────────────────────────────────────────────── @@ -461,53 +468,50 @@ static ToolDef checkWordCountTool() { @Timeout(value = 600, unit = TimeUnit.SECONDS) void testReportGeneration() { List tools = List.of( - createDirectoryTool(), - writeFileTool(), - readFileTool(), - assembleFilesTool(), - checkWordCountTool() - ); + createDirectoryTool(), writeFileTool(), readFileTool(), assembleFilesTool(), checkWordCountTool()); Agent planner = Agent.builder() - .name("test_java_planner") - .model(MODEL) - .instructions(PLANNER_INSTRUCTIONS) - .maxTurns(3) - .maxTokens(4000) - .build(); + .name("test_java_planner") + .model(MODEL) + .instructions(PLANNER_INSTRUCTIONS) + .maxTurns(3) + .maxTokens(4000) + .build(); Agent fallback = Agent.builder() - .name("test_java_fallback") - .model(MODEL) - .instructions(FALLBACK_INSTRUCTIONS) - .tools(tools) - .maxTurns(10) - .maxTokens(8000) - .build(); + .name("test_java_fallback") + .model(MODEL) + .instructions(FALLBACK_INSTRUCTIONS) + .tools(tools) + .maxTurns(10) + .maxTokens(8000) + .build(); Agent harness = Agent.builder() - .name("test_java_report_gen") - .model(MODEL) - .tools(tools) - .planner(planner) - .fallback(fallback) - .strategy(Strategy.PLAN_EXECUTE) - .fallbackMaxTurns(5) - .build(); - - AgentResult result = runtime.run(harness, - "Write a short research report about: The impact of AI on software testing"); + .name("test_java_report_gen") + .model(MODEL) + .tools(tools) + .planner(planner) + .fallback(fallback) + .strategy(Strategy.PLAN_EXECUTE) + .fallbackMaxTurns(5) + .build(); + + AgentResult result = + runtime.run(harness, "Write a short research report about: The impact of AI on software testing"); // 1. Workflow completed - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Agent did not complete. Status: " + result.getStatus() - + ". Error: " + result.getError()); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent did not complete. Status: " + result.getStatus() + ". Error: " + result.getError()); // 2. Report file exists Path reportPath = WORK_DIR.resolve("report.md"); - assertTrue(Files.exists(reportPath), - "Report file not found at " + reportPath - + ". COUNTERFACTUAL: if tool workers didn't execute, no files are created."); + assertTrue( + Files.exists(reportPath), + "Report file not found at " + reportPath + + ". COUNTERFACTUAL: if tool workers didn't execute, no files are created."); // 3. Report has content String content; @@ -522,30 +526,31 @@ void testReportGeneration() { int wordCount = content.split("\\s+").length; // 4. Word count meets minimum - assertTrue(wordCount >= MIN_WORD_COUNT, - "Report has " + wordCount + " words, expected >= " + MIN_WORD_COUNT - + ". COUNTERFACTUAL: if plan execution skipped write steps, word count is 0."); + assertTrue( + wordCount >= MIN_WORD_COUNT, + "Report has " + wordCount + " words, expected >= " + MIN_WORD_COUNT + + ". COUNTERFACTUAL: if plan execution skipped write steps, word count is 0."); // 5. Section files were created (proves parallel execution happened) Path sectionsDir = WORK_DIR.resolve("sections"); - assertTrue(Files.isDirectory(sectionsDir), - "sections/ directory not created. " - + "COUNTERFACTUAL: if create_directory tool didn't run, this directory won't exist."); + assertTrue( + Files.isDirectory(sectionsDir), + "sections/ directory not created. " + + "COUNTERFACTUAL: if create_directory tool didn't run, this directory won't exist."); - File[] sectionFiles = sectionsDir.toFile().listFiles( - (dir, name) -> name.endsWith(".md")); + File[] sectionFiles = sectionsDir.toFile().listFiles((dir, name) -> name.endsWith(".md")); assertNotNull(sectionFiles, "Could not list section files"); - assertTrue(sectionFiles.length >= 2, - "Expected >= 2 section files, found " + sectionFiles.length - + ". COUNTERFACTUAL: parallel write_file steps must each produce a file."); + assertTrue( + sectionFiles.length >= 2, + "Expected >= 2 section files, found " + sectionFiles.length + + ". COUNTERFACTUAL: parallel write_file steps must each produce a file."); // 6. Each section file has content for (File sf : sectionFiles) { try { String sfContent = Files.readString(sf.toPath()); int sfWords = sfContent.split("\\s+").length; - assertTrue(sfWords > 10, - "Section " + sf.getName() + " has only " + sfWords + " words"); + assertTrue(sfWords > 10, "Section " + sf.getName() + " has only " + sfWords + " words"); } catch (IOException e) { fail("Failed to read section file " + sf.getName() + ": " + e.getMessage()); } @@ -565,47 +570,43 @@ void testReportGeneration() { @Timeout(value = 600, unit = TimeUnit.SECONDS) void testMaxTokensInGenerate() { List tools = List.of( - createDirectoryTool(), - writeFileTool(), - readFileTool(), - assembleFilesTool(), - checkWordCountTool() - ); + createDirectoryTool(), writeFileTool(), readFileTool(), assembleFilesTool(), checkWordCountTool()); Agent planner = Agent.builder() - .name("test_java_planner_maxtok") - .model(MODEL) - .instructions(MAX_TOKENS_PLANNER_INSTRUCTIONS) - .maxTurns(3) - .maxTokens(4000) - .build(); + .name("test_java_planner_maxtok") + .model(MODEL) + .instructions(MAX_TOKENS_PLANNER_INSTRUCTIONS) + .maxTurns(3) + .maxTokens(4000) + .build(); Agent fallback = Agent.builder() - .name("test_java_fallback_maxtok") - .model(MODEL) - .instructions(FALLBACK_INSTRUCTIONS) - .tools(tools) - .maxTurns(10) - .maxTokens(8000) - .build(); + .name("test_java_fallback_maxtok") + .model(MODEL) + .instructions(FALLBACK_INSTRUCTIONS) + .tools(tools) + .maxTurns(10) + .maxTokens(8000) + .build(); Agent harness = Agent.builder() - .name("test_java_report_gen_maxtok") - .model(MODEL) - .tools(tools) - .planner(planner) - .fallback(fallback) - .strategy(Strategy.PLAN_EXECUTE) - .fallbackMaxTurns(5) - .build(); - - AgentResult result = runtime.run(harness, - "Write a detailed research report about: Quantum computing applications in cryptography"); + .name("test_java_report_gen_maxtok") + .model(MODEL) + .tools(tools) + .planner(planner) + .fallback(fallback) + .strategy(Strategy.PLAN_EXECUTE) + .fallbackMaxTurns(5) + .build(); + + AgentResult result = runtime.run( + harness, "Write a detailed research report about: Quantum computing applications in cryptography"); // 1. Workflow completed — proves max_tokens field didn't break compilation - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Agent did not complete. Status: " + result.getStatus() - + ". Error: " + result.getError()); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent did not complete. Status: " + result.getStatus() + ". Error: " + result.getError()); // 2. We used to assert ``report.md`` exists, but the planner LLM // names the final output file unpredictably across runs (report.txt, @@ -619,13 +620,12 @@ void testMaxTokensInGenerate() { // tests/e2e/test_suite20_plan_execute.test.ts. List textFiles; try (var stream = Files.walk(WORK_DIR)) { - textFiles = stream - .filter(Files::isRegularFile) - .filter(p -> { - String n = p.getFileName().toString(); - return n.endsWith(".md") || n.endsWith(".txt"); - }) - .collect(java.util.stream.Collectors.toList()); + textFiles = stream.filter(Files::isRegularFile) + .filter(p -> { + String n = p.getFileName().toString(); + return n.endsWith(".md") || n.endsWith(".txt"); + }) + .collect(java.util.stream.Collectors.toList()); } catch (IOException e) { fail("Failed to walk WORK_DIR: " + e.getMessage()); return; @@ -642,8 +642,8 @@ void testMaxTokensInGenerate() { } int wordCount = all.length() == 0 ? 0 : all.toString().trim().split("\\s+").length; System.err.println("[testMaxTokensInGenerate] produced " + textFiles.size() - + " text file(s), total word count: " + wordCount - + ", files=" + textFiles); + + " text file(s), total word count: " + wordCount + + ", files=" + textFiles); // If the file-count assertion is about to fail, dump diagnostics // FIRST so the failure message tells us what actually happened — @@ -656,15 +656,17 @@ void testMaxTokensInGenerate() { dumpWorkflowDiagnostics(result.getExecutionId(), "testMaxTokensInGenerate"); } - assertTrue(textFiles.size() > 0, - "no .md/.txt files produced in " + WORK_DIR - + ". COUNTERFACTUAL: if the GraalJS compiler dropped max_tokens, " - + "the workflow may have terminated before writing any output." - + " See stderr for workflow diagnostics."); - assertTrue(wordCount >= MIN_WORD_COUNT, - "Total word count " + wordCount + " < " + MIN_WORD_COUNT - + ". COUNTERFACTUAL: if max_tokens was ignored, LLM output is truncated short." - + " See stderr for workflow diagnostics."); + assertTrue( + textFiles.size() > 0, + "no .md/.txt files produced in " + WORK_DIR + + ". COUNTERFACTUAL: if the GraalJS compiler dropped max_tokens, " + + "the workflow may have terminated before writing any output." + + " See stderr for workflow diagnostics."); + assertTrue( + wordCount >= MIN_WORD_COUNT, + "Total word count " + wordCount + " < " + MIN_WORD_COUNT + + ". COUNTERFACTUAL: if max_tokens was ignored, LLM output is truncated short." + + " See stderr for workflow diagnostics."); } /** @@ -700,7 +702,9 @@ private void dumpWorkflowDiagnostics(String executionId, String label) { Object output = wf.get("output"); if (output != null) { System.err.println(" parent output keys: " - + (output instanceof Map m ? m.keySet() : output.getClass().getSimpleName())); + + (output instanceof Map m + ? m.keySet() + : output.getClass().getSimpleName())); } List> tasks = (List>) wf.getOrDefault("tasks", List.of()); System.err.println(" task count: " + tasks.size()); @@ -803,15 +807,15 @@ private void dumpChildWorkflow(String executionId, String indent) { private static Map fetchWorkflowWithTasks(String executionId) { try { java.net.http.HttpClient http = java.net.http.HttpClient.newBuilder() - .connectTimeout(java.time.Duration.ofSeconds(10)) - .build(); + .connectTimeout(java.time.Duration.ofSeconds(10)) + .build(); java.net.http.HttpRequest req = java.net.http.HttpRequest.newBuilder() - .uri(java.net.URI.create(BASE_URL + "/api/workflow/" + executionId + "?includeTasks=true")) - .timeout(java.time.Duration.ofSeconds(10)) - .GET() - .build(); - java.net.http.HttpResponse resp = http.send(req, - java.net.http.HttpResponse.BodyHandlers.ofString()); + .uri(java.net.URI.create(BASE_URL + "/api/workflow/" + executionId + "?includeTasks=true")) + .timeout(java.time.Duration.ofSeconds(10)) + .GET() + .build(); + java.net.http.HttpResponse resp = + http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString()); if (resp.statusCode() >= 400) return null; return new com.fasterxml.jackson.databind.ObjectMapper().readValue(resp.body(), Map.class); } catch (Exception e) { @@ -833,82 +837,85 @@ private static String truncate(String s, int max) { static ToolDef jProduceTool() { return ToolDef.builder() - .name("j_s20_produce") - .description("Step A — emit a known record.") - .inputSchema(Map.of( - "type", "object", - "properties", Map.of("record_id", Map.of("type", "string")), - "required", List.of("record_id"))) - .toolType("worker") - .func(input -> Map.of( - "record_id", input.get("record_id"), - "value", 42, - "tags", List.of("alpha", "beta"))) - .build(); + .name("j_s20_produce") + .description("Step A — emit a known record.") + .inputSchema(Map.of( + "type", "object", + "properties", Map.of("record_id", Map.of("type", "string")), + "required", List.of("record_id"))) + .toolType("worker") + .func(input -> Map.of( + "record_id", input.get("record_id"), + "value", 42, + "tags", List.of("alpha", "beta"))) + .build(); } static ToolDef jEnrichTool() { return ToolDef.builder() - .name("j_s20_enrich") - .description("Step B — read Step A via Ref.") - .inputSchema(Map.of( - "type", "object", - "properties", Map.of("record", Map.of("type", "object")), - "required", List.of("record"))) - .toolType("worker") - .func(input -> { - @SuppressWarnings("unchecked") - Map record = (Map) input.get("record"); - Map out = new LinkedHashMap<>(record); - int value = ((Number) record.getOrDefault("value", 0)).intValue(); - out.put("value_squared", value * value); - return out; - }) - .build(); + .name("j_s20_enrich") + .description("Step B — read Step A via Ref.") + .inputSchema(Map.of( + "type", "object", + "properties", Map.of("record", Map.of("type", "object")), + "required", List.of("record"))) + .toolType("worker") + .func(input -> { + @SuppressWarnings("unchecked") + Map record = (Map) input.get("record"); + Map out = new LinkedHashMap<>(record); + int value = ((Number) record.getOrDefault("value", 0)).intValue(); + out.put("value_squared", value * value); + return out; + }) + .build(); } static ToolDef jReportTool() { return ToolDef.builder() - .name("j_s20_report") - .description("Step C — read BOTH upstream steps.") - .inputSchema(Map.of( - "type", "object", - "properties", Map.of( - "record", Map.of("type", "object"), - "enriched", Map.of("type", "object")), - "required", List.of("record", "enriched"))) - .toolType("worker") - .func(input -> { - @SuppressWarnings("unchecked") - Map record = (Map) input.get("record"); - @SuppressWarnings("unchecked") - Map enriched = (Map) input.get("enriched"); - @SuppressWarnings("unchecked") - List tags = (List) record.get("tags"); - Map out = new LinkedHashMap<>(); - out.put("id", record.get("record_id")); - out.put("original_value", record.get("value")); - out.put("squared", enriched.get("value_squared")); - out.put("tags_joined", String.join( - ", ", tags.stream().map(Object::toString).toList())); - return out; - }) - .build(); + .name("j_s20_report") + .description("Step C — read BOTH upstream steps.") + .inputSchema(Map.of( + "type", "object", + "properties", + Map.of( + "record", Map.of("type", "object"), + "enriched", Map.of("type", "object")), + "required", List.of("record", "enriched"))) + .toolType("worker") + .func(input -> { + @SuppressWarnings("unchecked") + Map record = (Map) input.get("record"); + @SuppressWarnings("unchecked") + Map enriched = (Map) input.get("enriched"); + @SuppressWarnings("unchecked") + List tags = (List) record.get("tags"); + Map out = new LinkedHashMap<>(); + out.put("id", record.get("record_id")); + out.put("original_value", record.get("value")); + out.put("squared", enriched.get("value_squared")); + out.put( + "tags_joined", + String.join( + ", ", tags.stream().map(Object::toString).toList())); + return out; + }) + .build(); } Agent buildRefsHarness() { Agent planner = Agent.builder() - .name("j_s20_refs_planner") - .model(MODEL) - .instructions("(planner unused; static plan supplied)") - .build(); + .name("j_s20_refs_planner") + .model(MODEL) + .instructions("(planner unused; static plan supplied)") + .build(); return Agent.builder() - .name("j_s20_refs_harness") - .model(MODEL) - .strategy(Strategy.PLAN_EXECUTE) - .planner(planner) - .tools(List.of(jProduceTool(), jEnrichTool(), jReportTool())) - .build(); + .name("j_s20_refs_harness") + .model(MODEL) + .strategy(Strategy.PLAN_EXECUTE) + .planner(planner) + .tools(List.of(jProduceTool(), jEnrichTool(), jReportTool())) + .build(); } @SuppressWarnings("unchecked") @@ -947,22 +954,24 @@ Map> fetchStepOutputs(String executionId) throws Exc void testRefPipesWholeOutputAcrossSteps() throws Exception { Agent harness = buildRefsHarness(); Plan plan = Plan.builder() - .step(Step.builder("a") - .operation(Op.builder("j_s20_produce") - .args(Map.of("record_id", "r-001")) - .build()) - .build()) - .step(Step.builder("b") - .dependsOn("a") - .operation(Op.builder("j_s20_enrich") - .args(Map.of("record", new Ref("a"))) - .build()) - .build()) - .build(); + .step(Step.builder("a") + .operation(Op.builder("j_s20_produce") + .args(Map.of("record_id", "r-001")) + .build()) + .build()) + .step(Step.builder("b") + .dependsOn("a") + .operation(Op.builder("j_s20_enrich") + .args(Map.of("record", new Ref("a"))) + .build()) + .build()) + .build(); AgentResult result = runtime.run(harness, "go", plan); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "workflow did not COMPLETE: status=" + result.getStatus() + " error=" + result.getError()); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "workflow did not COMPLETE: status=" + result.getStatus() + " error=" + result.getError()); Map> outputs = fetchStepOutputs(result.getExecutionId()); @@ -974,10 +983,11 @@ void testRefPipesWholeOutputAcrossSteps() throws Exception { Map enrich = outputs.get("j_s20_enrich"); assertNotNull(enrich, "enrich step did not run — Ref likely unwired"); assertEquals( - 1764, ((Number) enrich.get("value_squared")).intValue(), - "value_squared must be 1764 (= 42²). If Ref didn't carry the dict, " - + "enrich would have received the literal {\"$ref\":\"a\"} marker and squared 0. " - + "Full enrich output: " + enrich); + 1764, + ((Number) enrich.get("value_squared")).intValue(), + "value_squared must be 1764 (= 42²). If Ref didn't carry the dict, " + + "enrich would have received the literal {\"$ref\":\"a\"} marker and squared 0. " + + "Full enrich output: " + enrich); assertEquals("r-001", enrich.get("record_id")); assertEquals(42, ((Number) enrich.get("value")).intValue()); } @@ -995,26 +1005,26 @@ void testRefPipesWholeOutputAcrossSteps() throws Exception { void testTwoRefsInSameArgsResolveIndependently() throws Exception { Agent harness = buildRefsHarness(); Plan plan = Plan.builder() - .step(Step.builder("a") - .operation(Op.builder("j_s20_produce") - .args(Map.of("record_id", "r-001")) - .build()) - .build()) - .step(Step.builder("b") - .dependsOn("a") - .operation(Op.builder("j_s20_enrich") - .args(Map.of("record", new Ref("a"))) - .build()) - .build()) - .step(Step.builder("c") - .dependsOn("a", "b") - .operation(Op.builder("j_s20_report") - .args(Map.of( - "record", new Ref("a"), - "enriched", new Ref("b"))) - .build()) - .build()) - .build(); + .step(Step.builder("a") + .operation(Op.builder("j_s20_produce") + .args(Map.of("record_id", "r-001")) + .build()) + .build()) + .step(Step.builder("b") + .dependsOn("a") + .operation(Op.builder("j_s20_enrich") + .args(Map.of("record", new Ref("a"))) + .build()) + .build()) + .step(Step.builder("c") + .dependsOn("a", "b") + .operation(Op.builder("j_s20_report") + .args(Map.of( + "record", new Ref("a"), + "enriched", new Ref("b"))) + .build()) + .build()) + .build(); AgentResult result = runtime.run(harness, "go", plan); assertEquals(AgentStatus.COMPLETED, result.getStatus()); diff --git a/sdk/java/e2e/Suite10CodeExecution.java b/sdk/java/e2e/Suite10CodeExecution.java index 0a41efa7d..4364d69d2 100644 --- a/sdk/java/e2e/Suite10CodeExecution.java +++ b/sdk/java/e2e/Suite10CodeExecution.java @@ -1,22 +1,23 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.execution.DockerCodeExecutor; -import ai.agentspan.execution.ExecutionResult; -import ai.agentspan.model.AgentResult; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assumptions.assumeTrue; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.execution.DockerCodeExecutor; +import org.conductoross.conductor.ai.execution.ExecutionResult; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.junit.jupiter.api.*; /** * Suite 9: Local Code Execution — plan-level and runtime tests for the @@ -47,7 +48,7 @@ class Suite10CodeExecution extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -67,15 +68,15 @@ private List> findExecuteCodeTasks(String executionId) { List> allTasks = (List>) workflow.get("tasks"); if (allTasks == null) return List.of(); return allTasks.stream() - .filter(t -> { - String ref = (String) t.getOrDefault("referenceTaskName", ""); - String defName = (String) t.getOrDefault("taskDefName", ""); - String taskType = (String) t.getOrDefault("taskType", ""); - return ref.contains("execute_code") - || defName.contains("execute_code") - || taskType.contains("execute_code"); - }) - .collect(Collectors.toList()); + .filter(t -> { + String ref = (String) t.getOrDefault("referenceTaskName", ""); + String defName = (String) t.getOrDefault("taskDefName", ""); + String taskType = (String) t.getOrDefault("taskType", ""); + return ref.contains("execute_code") + || defName.contains("execute_code") + || taskType.contains("execute_code"); + }) + .collect(Collectors.toList()); } /** Convert a task's outputData to a string for searching. */ @@ -99,69 +100,76 @@ private String taskOutputStr(Map task) { @SuppressWarnings("unchecked") void test_code_execution_compiles() { Agent agent = Agent.builder() - .name("e2e_java_ce_compile") - .model(MODEL) - .instructions("You can run code.") - .localCodeExecution(true) - .allowedLanguages(List.of("python", "bash")) - .codeExecutionTimeout(30) - .build(); - - Map plan = runtime.plan(agent); + .name("e2e_java_ce_compile") + .model(MODEL) + .instructions("You can run code.") + .localCodeExecution(true) + .allowedLanguages(List.of("python", "bash")) + .codeExecutionTimeout(30) + .build(); + + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); // Assert codeExecution block exists Map codeExec = (Map) agentDef.get("codeExecution"); - assertNotNull(codeExec, - "agentDef has no 'codeExecution' key — localCodeExecution not serialized. " - + "agentDef keys: " + agentDef.keySet() - + ". COUNTERFACTUAL: if codeExecution is not serialized, this fails."); + assertNotNull( + codeExec, + "agentDef has no 'codeExecution' key — localCodeExecution not serialized. " + + "agentDef keys: " + agentDef.keySet() + + ". COUNTERFACTUAL: if codeExecution is not serialized, this fails."); // enabled == true - assertEquals(true, codeExec.get("enabled"), - "codeExecution.enabled should be true. Got: " + codeExec.get("enabled") - + ". COUNTERFACTUAL: if the enabled flag is not set, this fails."); + assertEquals( + true, + codeExec.get("enabled"), + "codeExecution.enabled should be true. Got: " + codeExec.get("enabled") + + ". COUNTERFACTUAL: if the enabled flag is not set, this fails."); // allowedLanguages contains python and bash List langs = (List) codeExec.get("allowedLanguages"); - assertNotNull(langs, - "codeExecution.allowedLanguages is null. codeExecution keys: " + codeExec.keySet()); - assertTrue(langs.contains("python"), - "Expected 'python' in allowedLanguages. Got: " + langs); - assertTrue(langs.contains("bash"), - "Expected 'bash' in allowedLanguages. Got: " + langs); + assertNotNull(langs, "codeExecution.allowedLanguages is null. codeExecution keys: " + codeExec.keySet()); + assertTrue(langs.contains("python"), "Expected 'python' in allowedLanguages. Got: " + langs); + assertTrue(langs.contains("bash"), "Expected 'bash' in allowedLanguages. Got: " + langs); // timeout == 30 Object timeout = codeExec.get("timeout"); assertNotNull(timeout, "codeExecution.timeout is null"); - assertEquals(30, ((Number) timeout).intValue(), - "Expected codeExecution.timeout == 30. Got: " + timeout - + ". COUNTERFACTUAL: if timeout is not serialized, this fails."); + assertEquals( + 30, + ((Number) timeout).intValue(), + "Expected codeExecution.timeout == 30. Got: " + timeout + + ". COUNTERFACTUAL: if timeout is not serialized, this fails."); // execute_code tool injected into agentDef.tools List> tools = (List>) agentDef.get("tools"); - assertNotNull(tools, - "agentDef has no 'tools' key — execute_code tool not injected. " - + "COUNTERFACTUAL: if tool injection is missing, the LLM cannot call execute_code."); + assertNotNull( + tools, + "agentDef has no 'tools' key — execute_code tool not injected. " + + "COUNTERFACTUAL: if tool injection is missing, the LLM cannot call execute_code."); List> execTools = tools.stream() - .filter(t -> { - String name = (String) t.getOrDefault("name", ""); - return name.contains("execute_code"); - }) - .collect(Collectors.toList()); - - assertFalse(execTools.isEmpty(), - "No tool containing 'execute_code' found in agentDef.tools. " - + "Tool names: " + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList()) - + ". COUNTERFACTUAL: if execute_code tool is not injected, LLM cannot call it."); + .filter(t -> { + String name = (String) t.getOrDefault("name", ""); + return name.contains("execute_code"); + }) + .collect(Collectors.toList()); + + assertFalse( + execTools.isEmpty(), + "No tool containing 'execute_code' found in agentDef.tools. " + + "Tool names: " + + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList()) + + ". COUNTERFACTUAL: if execute_code tool is not injected, LLM cannot call it."); Map execTool = execTools.get(0); - assertEquals("e2e_java_ce_compile_execute_code", execTool.get("name"), - "Expected tool name 'e2e_java_ce_compile_execute_code'. Got: " + execTool.get("name") - + ". COUNTERFACTUAL: if tool naming is wrong, workers won't dispatch correctly."); - assertEquals("worker", execTool.get("toolType"), - "Expected toolType 'worker'. Got: " + execTool.get("toolType")); + assertEquals( + "e2e_java_ce_compile_execute_code", + execTool.get("name"), + "Expected tool name 'e2e_java_ce_compile_execute_code'. Got: " + execTool.get("name") + + ". COUNTERFACTUAL: if tool naming is wrong, workers won't dispatch correctly."); + assertEquals( + "worker", execTool.get("toolType"), "Expected toolType 'worker'. Got: " + execTool.get("toolType")); } /** @@ -177,44 +185,48 @@ void test_code_execution_compiles() { @SuppressWarnings("unchecked") void test_tool_naming_no_collision() { Agent agentA = Agent.builder() - .name("e2e_java_ce_a") - .model(MODEL) - .instructions("Run code.") - .localCodeExecution(true) - .allowedLanguages(List.of("python")) - .build(); + .name("e2e_java_ce_a") + .model(MODEL) + .instructions("Run code.") + .localCodeExecution(true) + .allowedLanguages(List.of("python")) + .build(); Agent agentB = Agent.builder() - .name("e2e_java_ce_b") - .model(MODEL) - .instructions("Run code.") - .localCodeExecution(true) - .allowedLanguages(List.of("python")) - .build(); + .name("e2e_java_ce_b") + .model(MODEL) + .instructions("Run code.") + .localCodeExecution(true) + .allowedLanguages(List.of("python")) + .build(); - Map planA = runtime.plan(agentA); - Map planB = runtime.plan(agentB); + CompileResponse planA = runtime.plan(agentA); + CompileResponse planB = runtime.plan(agentB); Map adA = getAgentDef(planA); Map adB = getAgentDef(planB); - List toolsA = ((List>) adA.get("tools")).stream() - .map(t -> (String) t.get("name")).collect(Collectors.toList()); - List toolsB = ((List>) adB.get("tools")).stream() - .map(t -> (String) t.get("name")).collect(Collectors.toList()); + List toolsA = ((List>) adA.get("tools")) + .stream().map(t -> (String) t.get("name")).collect(Collectors.toList()); + List toolsB = ((List>) adB.get("tools")) + .stream().map(t -> (String) t.get("name")).collect(Collectors.toList()); - assertTrue(toolsA.contains("e2e_java_ce_a_execute_code"), - "'e2e_java_ce_a_execute_code' not in agentA tools: " + toolsA - + ". COUNTERFACTUAL: if naming is wrong, tool won't dispatch to correct worker."); - assertTrue(toolsB.contains("e2e_java_ce_b_execute_code"), - "'e2e_java_ce_b_execute_code' not in agentB tools: " + toolsB); + assertTrue( + toolsA.contains("e2e_java_ce_a_execute_code"), + "'e2e_java_ce_a_execute_code' not in agentA tools: " + toolsA + + ". COUNTERFACTUAL: if naming is wrong, tool won't dispatch to correct worker."); + assertTrue( + toolsB.contains("e2e_java_ce_b_execute_code"), + "'e2e_java_ce_b_execute_code' not in agentB tools: " + toolsB); // No cross-contamination - assertFalse(toolsA.contains("e2e_java_ce_b_execute_code"), - "agentA has agentB's tool name — collision! toolsA=" + toolsA - + ". COUNTERFACTUAL: if naming collapses, both agents share the same worker."); - assertFalse(toolsB.contains("e2e_java_ce_a_execute_code"), - "agentB has agentA's tool name — collision! toolsB=" + toolsB); + assertFalse( + toolsA.contains("e2e_java_ce_b_execute_code"), + "agentA has agentB's tool name — collision! toolsA=" + toolsA + + ". COUNTERFACTUAL: if naming collapses, both agents share the same worker."); + assertFalse( + toolsB.contains("e2e_java_ce_a_execute_code"), + "agentB has agentA's tool name — collision! toolsB=" + toolsB); } /** @@ -233,14 +245,14 @@ void test_tool_naming_no_collision() { @SuppressWarnings("unchecked") void test_language_restriction_plan() { Agent agent = Agent.builder() - .name("e2e_java_ce_py_only") - .model(MODEL) - .instructions("You can only run Python code.") - .localCodeExecution(true) - .allowedLanguages(List.of("python")) // bash NOT allowed - .build(); - - Map plan = runtime.plan(agent); + .name("e2e_java_ce_py_only") + .model(MODEL) + .instructions("You can only run Python code.") + .localCodeExecution(true) + .allowedLanguages(List.of("python")) // bash NOT allowed + .build(); + + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); Map codeExec = (Map) agentDef.get("codeExecution"); @@ -249,12 +261,14 @@ void test_language_restriction_plan() { List allowed = (List) codeExec.get("allowedLanguages"); assertNotNull(allowed, "codeExecution.allowedLanguages is null"); - assertTrue(allowed.contains("python"), - "'python' not in allowedLanguages: " + allowed - + ". COUNTERFACTUAL: if python is missing, the restriction is broken."); - assertFalse(allowed.contains("bash"), - "'bash' should NOT be in allowedLanguages: " + allowed - + ". COUNTERFACTUAL: if bash leaks into allowedLanguages, restriction is broken."); + assertTrue( + allowed.contains("python"), + "'python' not in allowedLanguages: " + allowed + + ". COUNTERFACTUAL: if python is missing, the restriction is broken."); + assertFalse( + allowed.contains("bash"), + "'bash' should NOT be in allowedLanguages: " + allowed + + ". COUNTERFACTUAL: if bash leaks into allowedLanguages, restriction is broken."); } // ── Runtime tests ───────────────────────────────────────────────────── @@ -277,45 +291,51 @@ void test_language_restriction_plan() { @SuppressWarnings("unchecked") void test_local_python_execution() { Agent agent = Agent.builder() - .name("e2e_java_ce_python") - .model(MODEL) - .instructions("You can execute code using the execute_code tool. " - + "When asked to run Python code, you MUST call execute_code with " - + "language='python' and the exact code provided. Do not compute mentally — " - + "always use the execute_code tool.") - .localCodeExecution(true) - .allowedLanguages(List.of("python")) - .maxTurns(5) - .build(); - - AgentResult result = runtime.run(agent, - "Run this exact Python code using execute_code: print(42 * 73)"); - - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Agent with Python code execution should complete. " - + "Status: " + result.getStatus() - + ". Error: " + result.getError()); + .name("e2e_java_ce_python") + .model(MODEL) + .instructions("You can execute code using the execute_code tool. " + + "When asked to run Python code, you MUST call execute_code with " + + "language='python' and the exact code provided. Do not compute mentally — " + + "always use the execute_code tool.") + .localCodeExecution(true) + .allowedLanguages(List.of("python")) + .maxTurns(5) + .build(); + + AgentResult result = runtime.run(agent, "Run this exact Python code using execute_code: print(42 * 73)"); + + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent with Python code execution should complete. " + + "Status: " + result.getStatus() + + ". Error: " + result.getError()); String executionId = result.getExecutionId(); assertNotNull(executionId, "executionId is null"); List> execTasks = findExecuteCodeTasks(executionId); - assertFalse(execTasks.isEmpty(), - "No execute_code task found in workflow. " - + "COUNTERFACTUAL: if execute_code tool not injected or worker not dispatched, " - + "no execute_code task appears."); + assertFalse( + execTasks.isEmpty(), + "No execute_code task found in workflow. " + + "COUNTERFACTUAL: if execute_code tool not injected or worker not dispatched, " + + "no execute_code task appears."); // Verify at least one task has "3066" in output - boolean foundOutput = execTasks.stream() - .anyMatch(t -> taskOutputStr(t).contains("3066")); - - assertTrue(foundOutput, - "Expected '3066' (42 * 73) in execute_code task output. " - + "execute_code task outputs: " + execTasks.stream() - .map(t -> taskOutputStr(t).substring(0, Math.min(200, taskOutputStr(t).length()))) - .collect(Collectors.toList()) - + ". COUNTERFACTUAL: if wrong code ran or output is malformed, '3066' won't appear."); + boolean foundOutput = execTasks.stream().anyMatch(t -> taskOutputStr(t).contains("3066")); + + assertTrue( + foundOutput, + "Expected '3066' (42 * 73) in execute_code task output. " + + "execute_code task outputs: " + + execTasks.stream() + .map(t -> taskOutputStr(t) + .substring( + 0, + Math.min(200, taskOutputStr(t).length()))) + .collect(Collectors.toList()) + + ". COUNTERFACTUAL: if wrong code ran or output is malformed, '3066' won't appear."); } /** @@ -331,43 +351,49 @@ void test_local_python_execution() { @SuppressWarnings("unchecked") void test_local_bash_execution() { Agent agent = Agent.builder() - .name("e2e_java_ce_bash") - .model(MODEL) - .instructions("You can execute code using the execute_code tool. " - + "When asked to run bash code, you MUST call execute_code with " - + "language='bash' and the exact code provided. Always use execute_code — " - + "never compute the answer yourself.") - .localCodeExecution(true) - .allowedLanguages(List.of("bash")) - .maxTurns(5) - .build(); - - AgentResult result = runtime.run(agent, - "Run a bash script using execute_code that prints: echo $((17 + 29))"); - - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Agent with bash code execution should complete. " - + "Status: " + result.getStatus() - + ". Error: " + result.getError()); + .name("e2e_java_ce_bash") + .model(MODEL) + .instructions("You can execute code using the execute_code tool. " + + "When asked to run bash code, you MUST call execute_code with " + + "language='bash' and the exact code provided. Always use execute_code — " + + "never compute the answer yourself.") + .localCodeExecution(true) + .allowedLanguages(List.of("bash")) + .maxTurns(5) + .build(); + + AgentResult result = runtime.run(agent, "Run a bash script using execute_code that prints: echo $((17 + 29))"); + + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent with bash code execution should complete. " + + "Status: " + result.getStatus() + + ". Error: " + result.getError()); String executionId = result.getExecutionId(); assertNotNull(executionId, "executionId is null"); List> execTasks = findExecuteCodeTasks(executionId); - assertFalse(execTasks.isEmpty(), - "No execute_code task found in workflow. " - + "COUNTERFACTUAL: if execute_code tool not injected, no task appears."); + assertFalse( + execTasks.isEmpty(), + "No execute_code task found in workflow. " + + "COUNTERFACTUAL: if execute_code tool not injected, no task appears."); - boolean foundOutput = execTasks.stream() - .anyMatch(t -> taskOutputStr(t).contains("46")); + boolean foundOutput = execTasks.stream().anyMatch(t -> taskOutputStr(t).contains("46")); - assertTrue(foundOutput, - "Expected '46' (17 + 29) in execute_code bash task output. " - + "execute_code task outputs: " + execTasks.stream() - .map(t -> taskOutputStr(t).substring(0, Math.min(200, taskOutputStr(t).length()))) - .collect(Collectors.toList()) - + ". COUNTERFACTUAL: if bash output is wrong, '46' won't appear."); + assertTrue( + foundOutput, + "Expected '46' (17 + 29) in execute_code bash task output. " + + "execute_code task outputs: " + + execTasks.stream() + .map(t -> taskOutputStr(t) + .substring( + 0, + Math.min(200, taskOutputStr(t).length()))) + .collect(Collectors.toList()) + + ". COUNTERFACTUAL: if bash output is wrong, '46' won't appear."); } /** @@ -387,25 +413,25 @@ void test_local_bash_execution() { @SuppressWarnings("unchecked") void test_local_timeout() { Agent agent = Agent.builder() - .name("e2e_java_ce_timeout") - .model(MODEL) - .instructions("You MUST use execute_code to run the exact Python code given. " - + "Do not modify the code. Always call execute_code — never simulate execution.") - .localCodeExecution(true) - .allowedLanguages(List.of("python")) - .codeExecutionTimeout(2) // 2-second timeout - .maxTurns(3) - .build(); - - AgentResult result = runtime.run(agent, - "Run this Python code using execute_code: import time; time.sleep(60); print('done')"); + .name("e2e_java_ce_timeout") + .model(MODEL) + .instructions("You MUST use execute_code to run the exact Python code given. " + + "Do not modify the code. Always call execute_code — never simulate execution.") + .localCodeExecution(true) + .allowedLanguages(List.of("python")) + .codeExecutionTimeout(2) // 2-second timeout + .maxTurns(3) + .build(); + + AgentResult result = runtime.run( + agent, "Run this Python code using execute_code: import time; time.sleep(60); print('done')"); // Accept COMPLETED/FAILED/TERMINATED — the LLM may report the timeout gracefully assertTrue( - result.getStatus() == AgentStatus.COMPLETED - || result.getStatus() == AgentStatus.FAILED - || result.getStatus() == AgentStatus.TERMINATED, - "Expected a terminal status. Got: " + result.getStatus()); + result.getStatus() == AgentStatus.COMPLETED + || result.getStatus() == AgentStatus.FAILED + || result.getStatus() == AgentStatus.TERMINATED, + "Expected a terminal status. Got: " + result.getStatus()); String executionId = result.getExecutionId(); assertNotNull(executionId, "executionId is null"); @@ -415,43 +441,48 @@ void test_local_timeout() { if (!execTasks.isEmpty()) { // Verify that the timeout error message appears in at least one task // The error field should contain "timed out" and exit_code should be -1 - boolean timeoutErrorFound = execTasks.stream() - .anyMatch(t -> { - Object outputData = t.get("outputData"); - if (!(outputData instanceof Map)) return false; - @SuppressWarnings("unchecked") - Map outMap = (Map) outputData; - Object errorVal = outMap.get("error"); - Object exitCode = outMap.get("exit_code"); - Object success = outMap.get("success"); - boolean hasTimeoutError = errorVal != null + boolean timeoutErrorFound = execTasks.stream().anyMatch(t -> { + Object outputData = t.get("outputData"); + if (!(outputData instanceof Map)) return false; + @SuppressWarnings("unchecked") + Map outMap = (Map) outputData; + Object errorVal = outMap.get("error"); + Object exitCode = outMap.get("exit_code"); + Object success = outMap.get("success"); + boolean hasTimeoutError = errorVal != null && (errorVal.toString().toLowerCase().contains("timed out") - || errorVal.toString().toLowerCase().contains("timeout")); - boolean timedOutByExit = exitCode instanceof Number - && ((Number) exitCode).intValue() == -1; - // The test's invariant: long-running code did NOT complete - // successfully. The happy path is timeout (exit_code == -1 + - // "timed out" message). But gpt-4o-mini occasionally emits - // syntactically invalid Python (stray indentation on - // ``time.sleep(60)``); the worker rejects it with exit_code - // 1 before any timeout fires. Either outcome proves the - // worker prevented the sleep from running for its full 60s - // — accept both. The negative assertion below - // (``done`` MUST NOT appear in stdout) is still the - // counterfactual we care about. - boolean executionPrevented = Boolean.FALSE.equals(success) + || errorVal.toString().toLowerCase().contains("timeout")); + boolean timedOutByExit = exitCode instanceof Number && ((Number) exitCode).intValue() == -1; + // The test's invariant: long-running code did NOT complete + // successfully. The happy path is timeout (exit_code == -1 + + // "timed out" message). But gpt-4o-mini occasionally emits + // syntactically invalid Python (stray indentation on + // ``time.sleep(60)``); the worker rejects it with exit_code + // 1 before any timeout fires. Either outcome proves the + // worker prevented the sleep from running for its full 60s + // — accept both. The negative assertion below + // (``done`` MUST NOT appear in stdout) is still the + // counterfactual we care about. + boolean executionPrevented = Boolean.FALSE.equals(success) || (exitCode instanceof Number && ((Number) exitCode).intValue() != 0); - return (hasTimeoutError && timedOutByExit) || executionPrevented; - }); - - assertTrue(timeoutErrorFound, - "Expected at least one execute_code task to be prevented from running — " - + "either by timing out (exit_code == -1, 'timed out' message) OR by " - + "rejecting bad code (non-zero exit, success=false). " - + "execute_code task outputs: " + execTasks.stream() - .map(t -> taskOutputStr(t).substring(0, Math.min(300, taskOutputStr(t).length()))) - .collect(Collectors.toList()) - + ". COUNTERFACTUAL: a successful long sleep would have exit_code == 0."); + return (hasTimeoutError && timedOutByExit) || executionPrevented; + }); + + assertTrue( + timeoutErrorFound, + "Expected at least one execute_code task to be prevented from running — " + + "either by timing out (exit_code == -1, 'timed out' message) OR by " + + "rejecting bad code (non-zero exit, success=false). " + + "execute_code task outputs: " + + execTasks.stream() + .map(t -> taskOutputStr(t) + .substring( + 0, + Math.min( + 300, + taskOutputStr(t).length()))) + .collect(Collectors.toList()) + + ". COUNTERFACTUAL: a successful long sleep would have exit_code == 0."); // No symmetric "no 'done' in any stdout" check — the LLM may // legitimately run multiple execute_code attempts across turns; // one may hit timeout while another (LLM rewrote the script @@ -474,7 +505,8 @@ void test_local_timeout() { private static boolean dockerAvailable() { try { Process p = new ProcessBuilder("docker", "--version") - .redirectErrorStream(true).start(); + .redirectErrorStream(true) + .start(); boolean finished = p.waitFor(5, TimeUnit.SECONDS); return finished && p.exitValue() == 0; } catch (Exception e) { @@ -501,16 +533,20 @@ void test_docker_executor_runs_python() { DockerCodeExecutor executor = new DockerCodeExecutor("python:3.12-slim", "python", 30); ExecutionResult result = executor.execute("print(42 * 73)"); - assertEquals(0, result.getExitCode(), - "DockerCodeExecutor must exit 0 for valid Python. output=" + result.getOutput() - + " error=" + result.getError() - + ". COUNTERFACTUAL: a broken docker invocation would produce a non-zero exit code."); - assertTrue(result.getOutput().contains("3066"), - "DockerCodeExecutor stdout must contain '3066' (42*73). Got output='" + result.getOutput() - + "', error='" + result.getError() + "'" - + ". COUNTERFACTUAL: empty/wrong stdout means the script never ran in the container."); - assertFalse(result.isTimedOut(), - "DockerCodeExecutor must NOT time out for a one-line print. timedOut=true is wrong."); + assertEquals( + 0, + result.getExitCode(), + "DockerCodeExecutor must exit 0 for valid Python. output=" + result.getOutput() + + " error=" + result.getError() + + ". COUNTERFACTUAL: a broken docker invocation would produce a non-zero exit code."); + assertTrue( + result.getOutput().contains("3066"), + "DockerCodeExecutor stdout must contain '3066' (42*73). Got output='" + result.getOutput() + + "', error='" + result.getError() + "'" + + ". COUNTERFACTUAL: empty/wrong stdout means the script never ran in the container."); + assertFalse( + result.isTimedOut(), + "DockerCodeExecutor must NOT time out for a one-line print. timedOut=true is wrong."); } /** @@ -529,22 +565,24 @@ void test_docker_executor_network_disabled() { assumeTrue(dockerAvailable(), "Docker is not available — skipping Docker network test."); DockerCodeExecutor executor = new DockerCodeExecutor("python:3.12-slim", "python", 20); - String code = - "import urllib.request, sys\n" - + "try:\n" - + " urllib.request.urlopen('http://example.com', timeout=5)\n" - + " print('NET_OK')\n" - + "except Exception as e:\n" - + " print('NET_FAIL:' + type(e).__name__, file=sys.stderr)\n" - + " sys.exit(2)\n"; + String code = "import urllib.request, sys\n" + + "try:\n" + + " urllib.request.urlopen('http://example.com', timeout=5)\n" + + " print('NET_OK')\n" + + "except Exception as e:\n" + + " print('NET_FAIL:' + type(e).__name__, file=sys.stderr)\n" + + " sys.exit(2)\n"; ExecutionResult result = executor.execute(code); - assertNotEquals(0, result.getExitCode(), - "DockerCodeExecutor with --network=none must REFUSE outbound HTTP. " - + "output=" + result.getOutput() + " error=" + result.getError() - + ". COUNTERFACTUAL: a zero exit code means network isolation isn't enforced."); - assertFalse(result.getOutput().contains("NET_OK"), - "stdout must NOT contain 'NET_OK' — the urlopen call must fail under --network=none. " - + "Got output='" + result.getOutput() + "'."); + assertNotEquals( + 0, + result.getExitCode(), + "DockerCodeExecutor with --network=none must REFUSE outbound HTTP. " + + "output=" + result.getOutput() + " error=" + result.getError() + + ". COUNTERFACTUAL: a zero exit code means network isolation isn't enforced."); + assertFalse( + result.getOutput().contains("NET_OK"), + "stdout must NOT contain 'NET_OK' — the urlopen call must fail under --network=none. " + "Got output='" + + result.getOutput() + "'."); } } diff --git a/sdk/java/e2e/Suite11LangChain4j.java b/sdk/java/e2e/Suite11LangChain4j.java index ae4e891c7..f1a2303f6 100644 --- a/sdk/java/e2e/Suite11LangChain4j.java +++ b/sdk/java/e2e/Suite11LangChain4j.java @@ -1,14 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.frameworks.LangChain4jAgent; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; @@ -16,7 +9,15 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.frameworks.LangChain4jAgent; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.conductoross.conductor.ai.model.ToolDef; +import org.junit.jupiter.api.*; /** * Suite 8: LangChain4j framework integration. @@ -44,7 +45,7 @@ class Suite11LangChain4j extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -56,9 +57,7 @@ static void teardown() { static class CalculatorTools { @dev.langchain4j.agent.tool.Tool(name = "lc4j_add", value = "Add two integers") - public int add( - @dev.langchain4j.agent.tool.P("a") int a, - @dev.langchain4j.agent.tool.P("b") int b) { + public int add(@dev.langchain4j.agent.tool.P("a") int a, @dev.langchain4j.agent.tool.P("b") int b) { // Side-effect: proves tool function body actually ran toolCalled.set(true); return a + b; @@ -82,18 +81,16 @@ public String greet(@dev.langchain4j.agent.tool.P("name") String name) { @Order(1) void test_framework_detection() { assertTrue( - LangChain4jAgent.isLangChain4jTools(new CalculatorTools()), - "isLangChain4jTools should return true for CalculatorTools (has @dev.langchain4j.agent.tool.Tool methods). " - + "COUNTERFACTUAL: if annotation detection is broken, this returns false."); + LangChain4jAgent.isLangChain4jTools(new CalculatorTools()), + "isLangChain4jTools should return true for CalculatorTools (has @dev.langchain4j.agent.tool.Tool methods). " + + "COUNTERFACTUAL: if annotation detection is broken, this returns false."); assertFalse( - LangChain4jAgent.isLangChain4jTools(new Object()), - "isLangChain4jTools should return false for plain Object (no @Tool methods). " - + "COUNTERFACTUAL: if detection always returns true, this assertion fails."); + LangChain4jAgent.isLangChain4jTools(new Object()), + "isLangChain4jTools should return false for plain Object (no @Tool methods). " + + "COUNTERFACTUAL: if detection always returns true, this assertion fails."); - assertFalse( - LangChain4jAgent.isLangChain4jTools(null), - "isLangChain4jTools should return false for null."); + assertFalse(LangChain4jAgent.isLangChain4jTools(null), "isLangChain4jTools should return false for null."); } /** @@ -108,50 +105,52 @@ void test_framework_detection() { @Order(2) @SuppressWarnings("unchecked") void test_tool_extraction() { - Agent agent = LangChain4jAgent.from( - "lc4j_extraction_test", - MODEL, - "You are a test agent.", - new CalculatorTools()); + Agent agent = + LangChain4jAgent.from("lc4j_extraction_test", MODEL, "You are a test agent.", new CalculatorTools()); List tools = agent.getTools(); // Correct count - assertEquals(2, tools.size(), - "Expected 2 tools from CalculatorTools, got " + tools.size() + ". " - + "COUNTERFACTUAL: if extractTools misses a @Tool method, count < 2."); + assertEquals( + 2, + tools.size(), + "Expected 2 tools from CalculatorTools, got " + tools.size() + ". " + + "COUNTERFACTUAL: if extractTools misses a @Tool method, count < 2."); // Extract by name List names = tools.stream().map(ToolDef::getName).collect(Collectors.toList()); - assertTrue(names.contains("lc4j_add"), - "Tool 'lc4j_add' not found. Got: " + names + ". " - + "COUNTERFACTUAL: if @Tool(name=...) is not read, name would be Java method name."); - assertTrue(names.contains("lc4j_greet"), - "Tool 'lc4j_greet' not found. Got: " + names + ". " - + "COUNTERFACTUAL: same as above."); + assertTrue( + names.contains("lc4j_add"), + "Tool 'lc4j_add' not found. Got: " + names + ". " + + "COUNTERFACTUAL: if @Tool(name=...) is not read, name would be Java method name."); + assertTrue( + names.contains("lc4j_greet"), + "Tool 'lc4j_greet' not found. Got: " + names + ". " + "COUNTERFACTUAL: same as above."); // Non-empty descriptions for (ToolDef tool : tools) { - assertNotNull(tool.getDescription(), - "Tool '" + tool.getName() + "' has null description."); - assertFalse(tool.getDescription().isEmpty(), - "Tool '" + tool.getName() + "' has empty description. " - + "COUNTERFACTUAL: if @Tool(value=...) is not read, description would be empty."); + assertNotNull(tool.getDescription(), "Tool '" + tool.getName() + "' has null description."); + assertFalse( + tool.getDescription().isEmpty(), + "Tool '" + tool.getName() + "' has empty description. " + + "COUNTERFACTUAL: if @Tool(value=...) is not read, description would be empty."); } // Valid JSON Schema for (ToolDef tool : tools) { Map schema = tool.getInputSchema(); - assertNotNull(schema, - "Tool '" + tool.getName() + "' has null inputSchema."); - assertEquals("object", schema.get("type"), - "Tool '" + tool.getName() + "' inputSchema.type != 'object'. " - + "Got: " + schema.get("type") + ". " - + "COUNTERFACTUAL: if schema generation is broken, type would be missing or wrong."); - assertTrue(schema.containsKey("properties"), - "Tool '" + tool.getName() + "' inputSchema missing 'properties' key. " - + "Got keys: " + schema.keySet() + ". " - + "COUNTERFACTUAL: if schema generation is broken, properties would be absent."); + assertNotNull(schema, "Tool '" + tool.getName() + "' has null inputSchema."); + assertEquals( + "object", + schema.get("type"), + "Tool '" + tool.getName() + "' inputSchema.type != 'object'. " + + "Got: " + schema.get("type") + ". " + + "COUNTERFACTUAL: if schema generation is broken, type would be missing or wrong."); + assertTrue( + schema.containsKey("properties"), + "Tool '" + tool.getName() + "' inputSchema missing 'properties' key. " + + "Got keys: " + schema.keySet() + ". " + + "COUNTERFACTUAL: if schema generation is broken, properties would be absent."); } // lc4j_add should have properties 'a' and 'b' @@ -161,21 +160,25 @@ void test_tool_extraction() { .orElseThrow(() -> new AssertionError("lc4j_add not found")); Map addSchema = addTool.getInputSchema(); Map addProps = (Map) addSchema.get("properties"); - assertTrue(addProps.containsKey("a"), - "lc4j_add schema missing property 'a'. Got: " + addProps.keySet() + ". " - + "COUNTERFACTUAL: if @P annotation not read and -parameters not set, property name would be 'arg0'."); - assertTrue(addProps.containsKey("b"), - "lc4j_add schema missing property 'b'. Got: " + addProps.keySet() + ". " - + "COUNTERFACTUAL: same as above."); + assertTrue( + addProps.containsKey("a"), + "lc4j_add schema missing property 'a'. Got: " + addProps.keySet() + ". " + + "COUNTERFACTUAL: if @P annotation not read and -parameters not set, property name would be 'arg0'."); + assertTrue( + addProps.containsKey("b"), + "lc4j_add schema missing property 'b'. Got: " + addProps.keySet() + ". " + + "COUNTERFACTUAL: same as above."); // lc4j_greet should have property 'name' ToolDef greetTool = tools.stream() .filter(t -> "lc4j_greet".equals(t.getName())) .findFirst() .orElseThrow(() -> new AssertionError("lc4j_greet not found")); - Map greetProps = (Map) greetTool.getInputSchema().get("properties"); - assertTrue(greetProps.containsKey("name"), - "lc4j_greet schema missing property 'name'. Got: " + greetProps.keySet()); + Map greetProps = + (Map) greetTool.getInputSchema().get("properties"); + assertTrue( + greetProps.containsKey("name"), + "lc4j_greet schema missing property 'name'. Got: " + greetProps.keySet()); } /** @@ -189,42 +192,41 @@ void test_tool_extraction() { @Order(3) @SuppressWarnings("unchecked") void test_compiles_via_server() { - Agent agent = LangChain4jAgent.from( - "lc4j_compile_test", - MODEL, - "You are a test agent.", - new CalculatorTools()); + Agent agent = LangChain4jAgent.from("lc4j_compile_test", MODEL, "You are a test agent.", new CalculatorTools()); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); - assertTrue(plan.containsKey("workflowDef"), - "plan() result missing 'workflowDef'. Got keys: " + plan.keySet() + ". " - + "COUNTERFACTUAL: if agent serialization is completely broken, plan() fails."); + assertTrue( + plan.getWorkflowDef() != null && !plan.getWorkflowDef().isEmpty(), + "plan() result missing 'workflowDef'. Got keys: " + "[workflowDef, requiredWorkers]" + ". " + + "COUNTERFACTUAL: if agent serialization is completely broken, plan() fails."); Map agentDef = getAgentDef(plan); List> tools = (List>) agentDef.get("tools"); - assertNotNull(tools, - "agentDef has no 'tools' key. " - + "COUNTERFACTUAL: if tool list serialization breaks, key is absent."); - assertEquals(2, tools.size(), - "Expected 2 tools in agentDef.tools, got " + tools.size() + ". " - + "COUNTERFACTUAL: if tool extraction is incomplete, fewer tools appear."); - - List toolNames = tools.stream() - .map(t -> (String) t.get("name")) - .collect(Collectors.toList()); - assertTrue(toolNames.contains("lc4j_add"), - "Tool 'lc4j_add' not found in compiled agentDef. Got: " + toolNames); - assertTrue(toolNames.contains("lc4j_greet"), - "Tool 'lc4j_greet' not found in compiled agentDef. Got: " + toolNames); + assertNotNull( + tools, + "agentDef has no 'tools' key. " + "COUNTERFACTUAL: if tool list serialization breaks, key is absent."); + assertEquals( + 2, + tools.size(), + "Expected 2 tools in agentDef.tools, got " + tools.size() + ". " + + "COUNTERFACTUAL: if tool extraction is incomplete, fewer tools appear."); + + List toolNames = tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList()); + assertTrue(toolNames.contains("lc4j_add"), "Tool 'lc4j_add' not found in compiled agentDef. Got: " + toolNames); + assertTrue( + toolNames.contains("lc4j_greet"), + "Tool 'lc4j_greet' not found in compiled agentDef. Got: " + toolNames); // Both must have toolType="worker" for (Map tool : tools) { - assertEquals("worker", tool.get("toolType"), - "Tool '" + tool.get("name") + "' has toolType='" + tool.get("toolType") - + "', expected 'worker'. " - + "COUNTERFACTUAL: if toolType is not set, server may reject the agent."); + assertEquals( + "worker", + tool.get("toolType"), + "Tool '" + tool.get("name") + "' has toolType='" + tool.get("toolType") + + "', expected 'worker'. " + + "COUNTERFACTUAL: if toolType is not set, server may reject the agent."); } } @@ -249,14 +251,17 @@ void test_runtime_tool_invocation() { AgentResult result = runtime.run(agent, "What is 7 + 8?"); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Agent did not complete. Status: " + result.getStatus() - + ". Error: " + result.getError() + ". " - + "COUNTERFACTUAL: if agent compilation or execution fails, status is not COMPLETED."); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent did not complete. Status: " + result.getStatus() + + ". Error: " + result.getError() + ". " + + "COUNTERFACTUAL: if agent compilation or execution fails, status is not COMPLETED."); - assertTrue(toolCalled.get(), - "The 'lc4j_add' tool function body was never called. " - + "COUNTERFACTUAL: if LangChain4j worker dispatch is broken (wrong function wrapped, " - + "wrong worker name, or worker not registered), the flag stays false."); + assertTrue( + toolCalled.get(), + "The 'lc4j_add' tool function body was never called. " + + "COUNTERFACTUAL: if LangChain4j worker dispatch is broken (wrong function wrapped, " + + "wrong worker name, or worker not registered), the flag stays false."); } } diff --git a/sdk/java/e2e/Suite11bOpenAIAgent.java b/sdk/java/e2e/Suite11bOpenAIAgent.java new file mode 100644 index 000000000..ae84c58f7 --- /dev/null +++ b/sdk/java/e2e/Suite11bOpenAIAgent.java @@ -0,0 +1,216 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.frameworks.OpenAIAgent; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.conductoross.conductor.ai.model.ToolDef; +import org.junit.jupiter.api.*; + +/** + * Suite 11b: OpenAI Agents SDK framework integration. + * + *

    Mirrors {@code Suite11LangChain4j} for the {@link OpenAIAgent} bridge, the + * way Python's framework e2e (e.g. {@code test_suite11_langgraph}) exercises a + * foreign-framework agent: detection/tagging, tool extraction, server + * compilation, and runtime execution. The server routes {@code framework="openai"} + * through its {@code OpenAINormalizer}. + *

      + *
    1. Framework tagging — {@link OpenAIAgent} builds an Agent with framework="openai"
    2. + *
    3. Tool extraction — correct names, descriptions, and JSON Schema
    4. + *
    5. Server compilation — agent compiles cleanly via {@code plan()}
    6. + *
    7. Runtime execution — tool function body actually runs end-to-end
    8. + *
    + * + *

    All validation is deterministic (no LLM output parsing for assertion). + */ +@Tag("e2e") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Timeout(value = 300, unit = TimeUnit.SECONDS) +class Suite11bOpenAIAgent extends BaseTest { + + private static AgentRuntime runtime; + + /** Set to {@code true} inside the {@code oai_add} tool body when it is actually invoked. */ + static final AtomicBoolean toolCalled = new AtomicBoolean(false); + + @BeforeAll + static void setup() { + runtime = new AgentRuntime(new AgentConfig(100, 1)); + } + + @AfterAll + static void teardown() { + if (runtime != null) runtime.close(); + } + + // ── Tool class (@Tool-annotated POJO; OpenAIAgent accepts the LangChain4j annotation) ── + + static class CalculatorTools { + @dev.langchain4j.agent.tool.Tool(name = "oai_add", value = "Add two integers") + public int add(@dev.langchain4j.agent.tool.P("a") int a, @dev.langchain4j.agent.tool.P("b") int b) { + // Side-effect: proves tool function body actually ran + toolCalled.set(true); + return a + b; + } + + @dev.langchain4j.agent.tool.Tool(name = "oai_greet", value = "Greet a person by name") + public String greet(@dev.langchain4j.agent.tool.P("name") String name) { + return "Hello, " + name + "!"; + } + } + + // ── Tests ───────────────────────────────────────────────────────────── + + /** + * OpenAIAgent.builder().build() tags the agent with framework="openai" and extracts + * the @Tool methods. This is the OpenAI analogue of LangChain4j's detection test. + * + * COUNTERFACTUAL: if the bridge forgets to set framework, getFramework() != "openai"; + * if tool extraction is broken, the tool list is empty. + */ + @Test + @Order(1) + void test_framework_tagging_and_tool_extraction() { + Agent agent = OpenAIAgent.builder() + .name("oai_extraction_test") + .model(MODEL) + .instructions("You are a test agent.") + .tools(new CalculatorTools()) + .build(); + + assertEquals( + "openai", + agent.getFramework(), + "OpenAIAgent must tag the agent with framework='openai'. Got: " + agent.getFramework() + + ". COUNTERFACTUAL: if the bridge omits .framework(\"openai\"), the server routes it " + + "through the wrong (or no) normalizer."); + + List tools = agent.getTools(); + assertEquals( + 2, + tools.size(), + "Expected 2 tools from CalculatorTools, got " + tools.size() + + ". COUNTERFACTUAL: if extractTools misses a @Tool method, count < 2."); + + List names = tools.stream().map(ToolDef::getName).collect(Collectors.toList()); + assertTrue( + names.contains("oai_add"), + "Tool 'oai_add' not found. Got: " + names + + ". COUNTERFACTUAL: if @Tool(name=...) is ignored, name would be the Java method name."); + assertTrue(names.contains("oai_greet"), "Tool 'oai_greet' not found. Got: " + names); + + // Valid JSON Schema with parameter names from @P + ToolDef add = tools.stream() + .filter(t -> "oai_add".equals(t.getName())) + .findFirst() + .orElseThrow(); + assertNotNull(add.getDescription()); + assertFalse( + add.getDescription().isEmpty(), + "oai_add description empty. COUNTERFACTUAL: if @Tool(value=...) is ignored, description is empty."); + Map schema = add.getInputSchema(); + assertEquals("object", schema.get("type"), "oai_add inputSchema.type != 'object'. Got: " + schema.get("type")); + @SuppressWarnings("unchecked") + Map props = (Map) schema.get("properties"); + assertTrue( + props.containsKey("a") && props.containsKey("b"), + "oai_add schema missing properties a/b. Got: " + props.keySet() + + ". COUNTERFACTUAL: if @P is ignored and -parameters is off, names would be arg0/arg1."); + } + + /** + * Agent from OpenAIAgent compiles via plan() (the server normalizes the + * framework="openai" agent into a native agentDef) and the normalized agentDef + * carries both tools as toolType="worker". + * + * NOTE: the compiled agentDef does NOT carry framework="openai" — compilation + * runs OpenAINormalizer, which consumes the framework tag and emits a native + * config. Framework tagging is asserted pre-compile in test_framework_tagging. + * + * COUNTERFACTUAL: if tool serialization/normalization breaks, the tools are absent. + */ + @Test + @Order(2) + @SuppressWarnings("unchecked") + void test_compiles_via_server() { + Agent agent = OpenAIAgent.builder() + .name("oai_compile_test") + .model(MODEL) + .instructions("You are a test agent.") + .tools(new CalculatorTools()) + .build(); + + CompileResponse plan = runtime.plan(agent); + assertTrue( + plan.getWorkflowDef() != null && !plan.getWorkflowDef().isEmpty(), + "plan() result missing 'workflowDef'. Got keys: " + "[workflowDef, requiredWorkers]" + + ". COUNTERFACTUAL: if framework-agent serialization is broken, the server rejects compile."); + + Map agentDef = getAgentDef(plan); + + List> tools = (List>) agentDef.get("tools"); + assertNotNull(tools, "agentDef has no 'tools' key."); + assertEquals( + 2, + tools.size(), + "Expected 2 tools in agentDef.tools, got " + tools.size() + + ". COUNTERFACTUAL: if a tool is dropped during openai normalization, count < 2."); + + // The OpenAINormalizer emits worker-backed tools keyed by `_worker_ref` + // (its presence is what marks the tool as a local worker; there is no + // separate `name`/`toolType` field in the normalized form). + List toolRefs = + tools.stream().map(t -> (String) t.get("_worker_ref")).collect(Collectors.toList()); + assertTrue( + toolRefs.contains("oai_add") && toolRefs.contains("oai_greet"), + "Compiled agentDef tools missing oai_add/oai_greet (_worker_ref). Got: " + toolRefs + + ". COUNTERFACTUAL: if normalization drops the worker binding, the refs are absent/renamed."); + } + + /** + * Running the agent end-to-end causes the oai_add tool function body to execute, + * proving the server normalizes framework="openai" and dispatches the worker tool. + * + * COUNTERFACTUAL: if the openai normalizer drops tools or worker dispatch breaks, + * toolCalled stays false; if compilation/execution fails, status != COMPLETED. + */ + @Test + @Order(3) + @Timeout(value = 300, unit = TimeUnit.SECONDS) + void test_runtime_tool_invocation() { + toolCalled.set(false); + + Agent agent = OpenAIAgent.builder() + .name("oai_runtime_test") + .model(MODEL) + .instructions("You MUST call the oai_add tool with a=7, b=8. Report the result.") + .tools(new CalculatorTools()) + .build(); + + AgentResult result = runtime.run(agent, "What is 7 + 8?"); + + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent did not complete. Status: " + result.getStatus() + ". Error: " + result.getError() + + ". COUNTERFACTUAL: if openai-framework compilation or execution fails, status != COMPLETED."); + assertTrue( + toolCalled.get(), + "The 'oai_add' tool function body was never called. " + + "COUNTERFACTUAL: if the openai normalizer drops the worker tool or worker dispatch is " + + "broken (wrong function wrapped, wrong worker name, not registered), the flag stays false."); + } +} diff --git a/sdk/java/e2e/Suite12HandoffApprove.java b/sdk/java/e2e/Suite12HandoffApprove.java index f78340f72..0e443e600 100644 --- a/sdk/java/e2e/Suite12HandoffApprove.java +++ b/sdk/java/e2e/Suite12HandoffApprove.java @@ -1,23 +1,24 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.enums.EventType; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentEvent; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.AgentStream; -import ai.agentspan.model.ToolDef; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.concurrent.TimeUnit; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.enums.EventType; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentEvent; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.AgentStream; +import org.conductoross.conductor.ai.model.ToolDef; +import org.junit.jupiter.api.*; /** * Suite 12: Handoff + Human-in-the-Loop. @@ -37,7 +38,7 @@ class Suite12HandoffApprove extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -48,10 +49,9 @@ static void teardown() { /** Approval-required tool that lives on the sub-agent. */ public static class ApprovalTools { @Tool( - name = "submit_change", - description = "Submit a configuration change after human approval", - approvalRequired = true - ) + name = "submit_change", + description = "Submit a configuration change after human approval", + approvalRequired = true) public String submitChange(String change) { return "Change submitted: " + change; } @@ -61,12 +61,12 @@ private Agent buildHandoffAgent(String name) { List approvalTools = ToolRegistry.fromInstance(new ApprovalTools()); Agent reviewer = Agent.builder() - .name(name + "_reviewer") - .model(MODEL) - .instructions("You submit configuration changes. Always call submit_change with the requested change.") - .tools(approvalTools) - .maxTurns(2) - .build(); + .name(name + "_reviewer") + .model(MODEL) + .instructions("You submit configuration changes. Always call submit_change with the requested change.") + .tools(approvalTools) + .maxTurns(2) + .build(); // maxTurns(1) on the parent bounds the orchestrator's DO_WHILE: one // LLM call routes the handoff and the loop exits. Without this, @@ -74,13 +74,14 @@ private Agent buildHandoffAgent(String name) { // sub-agent replies, queueing another HUMAN approval that the test // never sees — the workflow hangs until the JUnit timeout fires. return Agent.builder() - .name(name) - .model(MODEL) - .instructions("Route every configuration change request to the reviewer sub-agent ONCE, then you are done. Do not answer directly.") - .agents(reviewer) - .strategy(Strategy.HANDOFF) - .maxTurns(1) - .build(); + .name(name) + .model(MODEL) + .instructions( + "Route every configuration change request to the reviewer sub-agent ONCE, then you are done. Do not answer directly.") + .agents(reviewer) + .strategy(Strategy.HANDOFF) + .maxTurns(1) + .build(); } /** @@ -93,8 +94,8 @@ private Agent buildHandoffAgent(String name) { void test_waiting_event_carries_sub_execution_id() { Agent support = buildHandoffAgent("e2e_java_handoff_waiting_id"); - try (AgentStream stream = runtime.stream(support, - "Please submit this configuration change: enable feature flag java_e2e_hitl.")) { + try (AgentStream stream = runtime.stream( + support, "Please submit this configuration change: enable feature flag java_e2e_hitl.")) { String topExecutionId = stream.getExecutionId(); assertNotNull(topExecutionId); @@ -116,9 +117,11 @@ void test_waiting_event_carries_sub_execution_id() { String waitingExecId = waiting.getExecutionId(); assertNotNull(waitingExecId, "WAITING event must carry an execution id"); assertFalse(waitingExecId.isEmpty(), "WAITING event execution id must not be empty"); - assertNotEquals(topExecutionId, waitingExecId, - "under HANDOFF the HUMAN task lives in the sub-execution, " - + "so the WAITING event id must differ from the top-level execution id"); + assertNotEquals( + topExecutionId, + waitingExecId, + "under HANDOFF the HUMAN task lives in the sub-execution, " + + "so the WAITING event id must differ from the top-level execution id"); } } @@ -149,7 +152,7 @@ void test_approve_with_event_completes_handoff_hitl() throws Exception { lastErr = e; if (attempt < maxAttempts) { System.err.println("[Suite12 HITL] attempt " + attempt + " failed (" - + e.getClass().getSimpleName() + "): " + e.getMessage() + " — retrying."); + + e.getClass().getSimpleName() + "): " + e.getMessage() + " — retrying."); } } } @@ -160,8 +163,8 @@ void test_approve_with_event_completes_handoff_hitl() throws Exception { private void runApproveWithEventOnce() throws Exception { Agent support = buildHandoffAgent("e2e_java_handoff_approve_event"); - try (AgentStream stream = runtime.stream(support, - "Please submit this configuration change: enable feature flag java_e2e_hitl.")) { + try (AgentStream stream = runtime.stream( + support, "Please submit this configuration change: enable feature flag java_e2e_hitl.")) { int approvals = 0; for (AgentEvent event : stream) { @@ -176,9 +179,11 @@ private void runApproveWithEventOnce() throws Exception { // Poll the server-side workflow status instead of waiting on the // original SSE stream, which won't see the post-approve resume. AgentResult result = stream.waitForResult(180_000, 1_000); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "workflow did not complete after approve(event). status=" + result.getStatus() - + " error=" + result.getError()); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "workflow did not complete after approve(event). status=" + result.getStatus() + " error=" + + result.getError()); } } @@ -191,11 +196,11 @@ private void runApproveWithEventOnce() throws Exception { @Order(3) void test_approve_with_event_rejects_empty_execution_id() { Agent solo = Agent.builder() - .name("e2e_java_handoff_guard") - .model(MODEL) - .instructions("Say hello.") - .maxTurns(1) - .build(); + .name("e2e_java_handoff_guard") + .model(MODEL) + .instructions("Say hello.") + .maxTurns(1) + .build(); try (AgentStream stream = runtime.stream(solo, "hi")) { for (AgentEvent ignored : stream) { @@ -203,24 +208,23 @@ void test_approve_with_event_rejects_empty_execution_id() { } AgentEvent eventWithNoId = new AgentEvent( - EventType.WAITING, - /*content*/ null, - /*toolName*/ "submit_change", - /*args*/ null, - /*result*/ null, - /*output*/ null, - /*executionId*/ "", - /*guardrailName*/ null, - /*target*/ null - ); - - IllegalArgumentException thrown = assertThrows( - IllegalArgumentException.class, - () -> stream.approve(eventWithNoId)); + EventType.WAITING, + /*content*/ null, + /*toolName*/ "submit_change", + /*args*/ null, + /*result*/ null, + /*output*/ null, + /*executionId*/ "", + /*guardrailName*/ null, + /*target*/ null); + + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> stream.approve(eventWithNoId)); assertNotNull(thrown.getMessage()); - assertTrue(thrown.getMessage().contains("execution id"), - "exception should mention the missing execution id, got: " + thrown.getMessage()); + assertTrue( + thrown.getMessage().contains("execution id"), + "exception should mention the missing execution id, got: " + thrown.getMessage()); } } } diff --git a/sdk/java/e2e/Suite12TerminationGates.java b/sdk/java/e2e/Suite12TerminationGates.java index 4306f0212..5b02c75d4 100644 --- a/sdk/java/e2e/Suite12TerminationGates.java +++ b/sdk/java/e2e/Suite12TerminationGates.java @@ -1,21 +1,21 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.model.AgentResult; -import ai.agentspan.termination.MaxMessageTermination; -import ai.agentspan.termination.TextMentionTermination; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.termination.MaxMessageTermination; +import org.conductoross.conductor.ai.termination.TextMentionTermination; +import org.junit.jupiter.api.*; /** * Suite 4: Termination — termination condition tests. @@ -36,7 +36,7 @@ class Suite12TerminationGates extends BaseTest { static void setup() { // Use BASE_URL (without /api suffix) since AgentConfig + HttpApi // already prepend /api to every path. - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -58,19 +58,21 @@ static void teardown() { @SuppressWarnings("unchecked") void test_max_message_termination() { Agent agent = Agent.builder() - .name("e2e_java_max_msg_agent") - .model(MODEL) - .instructions("After each message, respond with a short reply. Never stop on your own.") - .maxTurns(25) - .termination(MaxMessageTermination.of(3)) - .build(); + .name("e2e_java_max_msg_agent") + .model(MODEL) + .instructions("After each message, respond with a short reply. Never stop on your own.") + .maxTurns(25) + .termination(MaxMessageTermination.of(3)) + .build(); AgentResult result = runtime.run(agent, "Start the conversation"); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Agent should COMPLETE when MaxMessageTermination is reached. " - + "Got: " + result.getStatus() - + ". Termination gates complete the loop, not fail it."); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent should COMPLETE when MaxMessageTermination is reached. " + + "Got: " + result.getStatus() + + ". Termination gates complete the loop, not fail it."); // Fetch the workflow and count DO_WHILE iterations String executionId = result.getExecutionId(); @@ -82,9 +84,8 @@ void test_max_message_termination() { // Find DO_WHILE tasks (the loop wrapper) List> doWhileTasks = allTasks.stream() - .filter(t -> "DO_WHILE".equals(t.get("taskType")) - || "DO_WHILE".equals(t.get("type"))) - .collect(Collectors.toList()); + .filter(t -> "DO_WHILE".equals(t.get("taskType")) || "DO_WHILE".equals(t.get("type"))) + .collect(Collectors.toList()); if (!doWhileTasks.isEmpty()) { // Each DO_WHILE task has an 'iteration' field @@ -92,10 +93,11 @@ void test_max_message_termination() { Object iterationObj = loopTask.get("iteration"); if (iterationObj instanceof Number) { int iterations = ((Number) iterationObj).intValue(); - assertTrue(iterations <= 5, - "DO_WHILE loop ran " + iterations + " iterations, expected <= 5. " - + "COUNTERFACTUAL: if MaxMessageTermination doesn't work, " - + "agent runs all 25 turns."); + assertTrue( + iterations <= 5, + "DO_WHILE loop ran " + iterations + " iterations, expected <= 5. " + + "COUNTERFACTUAL: if MaxMessageTermination doesn't work, " + + "agent runs all 25 turns."); } } // Even if we can't find the DO_WHILE task, the COMPLETED status is the key assertion @@ -111,17 +113,19 @@ void test_max_message_termination() { @Timeout(value = 120, unit = TimeUnit.SECONDS) void test_invalid_model_fails() { Agent agent = Agent.builder() - .name("e2e_java_bad_model") - .model("nonexistent/xyz-model-does-not-exist") - .instructions("This agent should never execute successfully.") - .build(); + .name("e2e_java_bad_model") + .model("nonexistent/xyz-model-does-not-exist") + .instructions("This agent should never execute successfully.") + .build(); AgentResult result = runtime.run(agent, "Hello."); - assertNotEquals(AgentStatus.COMPLETED, result.getStatus(), - "[invalid model] Expected FAILED or TERMINATED for nonexistent model, " - + "got COMPLETED. The server should reject unknown models. " - + "COUNTERFACTUAL: if model validation is broken, this returns COMPLETED."); + assertNotEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "[invalid model] Expected FAILED or TERMINATED for nonexistent model, " + + "got COMPLETED. The server should reject unknown models. " + + "COUNTERFACTUAL: if model validation is broken, this returns COMPLETED."); } /** @@ -136,19 +140,21 @@ void test_invalid_model_fails() { @SuppressWarnings("unchecked") void test_text_mention_termination() { Agent agent = Agent.builder() - .name("e2e_java_text_term_agent") - .model(MODEL) - .instructions("Always include the text DONE_E2E in every response.") - .maxTurns(25) - .termination(TextMentionTermination.of("DONE_E2E")) - .build(); + .name("e2e_java_text_term_agent") + .model(MODEL) + .instructions("Always include the text DONE_E2E in every response.") + .maxTurns(25) + .termination(TextMentionTermination.of("DONE_E2E")) + .build(); AgentResult result = runtime.run(agent, "Begin"); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Agent should COMPLETE when TextMentionTermination is triggered. " - + "Got: " + result.getStatus() - + ". Termination gates complete the loop, not fail it."); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent should COMPLETE when TextMentionTermination is triggered. " + + "Got: " + result.getStatus() + + ". Termination gates complete the loop, not fail it."); // Fetch the workflow and check iterations stayed low String executionId = result.getExecutionId(); @@ -160,19 +166,19 @@ void test_text_mention_termination() { // Find DO_WHILE tasks List> doWhileTasks = allTasks.stream() - .filter(t -> "DO_WHILE".equals(t.get("taskType")) - || "DO_WHILE".equals(t.get("type"))) - .collect(Collectors.toList()); + .filter(t -> "DO_WHILE".equals(t.get("taskType")) || "DO_WHILE".equals(t.get("type"))) + .collect(Collectors.toList()); if (!doWhileTasks.isEmpty()) { Map loopTask = doWhileTasks.get(0); Object iterationObj = loopTask.get("iteration"); if (iterationObj instanceof Number) { int iterations = ((Number) iterationObj).intValue(); - assertTrue(iterations <= 3, - "DO_WHILE loop ran " + iterations + " iterations, expected <= 3. " - + "COUNTERFACTUAL: if TextMentionTermination doesn't fire on " - + "'DONE_E2E', agent runs 25 turns."); + assertTrue( + iterations <= 3, + "DO_WHILE loop ran " + iterations + " iterations, expected <= 3. " + + "COUNTERFACTUAL: if TextMentionTermination doesn't fire on " + + "'DONE_E2E', agent runs 25 turns."); } } // Even if we can't find the DO_WHILE task, the COMPLETED status is the key assertion diff --git a/sdk/java/e2e/Suite13Callbacks.java b/sdk/java/e2e/Suite13Callbacks.java index e7b73eaa8..4f3e9889e 100644 --- a/sdk/java/e2e/Suite13Callbacks.java +++ b/sdk/java/e2e/Suite13Callbacks.java @@ -1,20 +1,21 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.CallbackHandler; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.model.AgentResult; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.CallbackHandler; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.junit.jupiter.api.*; /** * Suite 7: Callbacks — CallbackHandler lifecycle hook tests. @@ -42,7 +43,7 @@ class Suite13Callbacks extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -94,17 +95,34 @@ public Map onToolEnd(Map kwargs) { /** Overrides all 6 lifecycle methods. */ static class AllHooksHandler extends CallbackHandler { @Override - public Map onAgentStart(Map kwargs) { return null; } + public Map onAgentStart(Map kwargs) { + return null; + } + @Override - public Map onAgentEnd(Map kwargs) { return null; } + public Map onAgentEnd(Map kwargs) { + return null; + } + @Override - public Map onModelStart(Map kwargs) { return null; } + public Map onModelStart(Map kwargs) { + return null; + } + @Override - public Map onModelEnd(Map kwargs) { return null; } + public Map onModelEnd(Map kwargs) { + return null; + } + @Override - public Map onToolStart(Map kwargs) { return null; } + public Map onToolStart(Map kwargs) { + return null; + } + @Override - public Map onToolEnd(Map kwargs) { return null; } + public Map onToolEnd(Map kwargs) { + return null; + } } // ── Tests ───────────────────────────────────────────────────────────── @@ -123,48 +141,51 @@ static class AllHooksHandler extends CallbackHandler { @SuppressWarnings("unchecked") void test_agent_callbacks_compile() { Agent agent = Agent.builder() - .name("e2e_java_agent_cb_compile") - .model(MODEL) - .instructions("You are a test agent.") - .callbacks(new AgentLifecycleHandler()) - .build(); + .name("e2e_java_agent_cb_compile") + .model(MODEL) + .instructions("You are a test agent.") + .callbacks(new AgentLifecycleHandler()) + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); List> callbacks = (List>) agentDef.get("callbacks"); - assertNotNull(callbacks, - "agentDef has no 'callbacks' key — CallbackHandler serialization is broken. " - + "agentDef keys: " + agentDef.keySet()); - assertFalse(callbacks.isEmpty(), - "agentDef.callbacks is empty — no callback positions serialized"); - - List positions = callbacks.stream() - .map(c -> (String) c.get("position")) - .collect(Collectors.toList()); - - assertTrue(positions.contains("before_agent"), - "Expected 'before_agent' in agentDef.callbacks positions. Found: " + positions - + ". COUNTERFACTUAL: if onAgentStart is not detected as an override, " - + "'before_agent' won't be serialized."); - assertTrue(positions.contains("after_agent"), - "Expected 'after_agent' in agentDef.callbacks positions. Found: " + positions - + ". COUNTERFACTUAL: if onAgentEnd is not detected as an override, " - + "'after_agent' won't be serialized."); + assertNotNull( + callbacks, + "agentDef has no 'callbacks' key — CallbackHandler serialization is broken. " + "agentDef keys: " + + agentDef.keySet()); + assertFalse(callbacks.isEmpty(), "agentDef.callbacks is empty — no callback positions serialized"); + + List positions = + callbacks.stream().map(c -> (String) c.get("position")).collect(Collectors.toList()); + + assertTrue( + positions.contains("before_agent"), + "Expected 'before_agent' in agentDef.callbacks positions. Found: " + positions + + ". COUNTERFACTUAL: if onAgentStart is not detected as an override, " + + "'before_agent' won't be serialized."); + assertTrue( + positions.contains("after_agent"), + "Expected 'after_agent' in agentDef.callbacks positions. Found: " + positions + + ". COUNTERFACTUAL: if onAgentEnd is not detected as an override, " + + "'after_agent' won't be serialized."); // Verify taskName pattern: {agentName}_{position} Map positionToTask = new java.util.HashMap<>(); for (Map cb : callbacks) { positionToTask.put((String) cb.get("position"), (String) cb.get("taskName")); } - assertEquals("e2e_java_agent_cb_compile_before_agent", - positionToTask.get("before_agent"), - "Expected taskName 'e2e_java_agent_cb_compile_before_agent'. " - + "Got: " + positionToTask.get("before_agent")); - assertEquals("e2e_java_agent_cb_compile_after_agent", - positionToTask.get("after_agent"), - "Expected taskName 'e2e_java_agent_cb_compile_after_agent'. " - + "Got: " + positionToTask.get("after_agent")); + assertEquals( + "e2e_java_agent_cb_compile_before_agent", + positionToTask.get("before_agent"), + "Expected taskName 'e2e_java_agent_cb_compile_before_agent'. " + "Got: " + + positionToTask.get("before_agent")); + assertEquals( + "e2e_java_agent_cb_compile_after_agent", + positionToTask.get("after_agent"), + "Expected taskName 'e2e_java_agent_cb_compile_after_agent'. " + "Got: " + + positionToTask.get("after_agent")); } /** @@ -177,31 +198,30 @@ void test_agent_callbacks_compile() { @SuppressWarnings("unchecked") void test_model_callbacks_compile() { Agent agent = Agent.builder() - .name("e2e_java_model_cb_compile") - .model(MODEL) - .instructions("You are a test agent.") - .callbacks(new ModelLifecycleHandler()) - .build(); + .name("e2e_java_model_cb_compile") + .model(MODEL) + .instructions("You are a test agent.") + .callbacks(new ModelLifecycleHandler()) + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); List> callbacks = (List>) agentDef.get("callbacks"); - assertNotNull(callbacks, - "agentDef has no 'callbacks' key — ModelLifecycleHandler serialization broken"); - assertFalse(callbacks.isEmpty(), - "agentDef.callbacks is empty — no model callbacks serialized"); - - List positions = callbacks.stream() - .map(c -> (String) c.get("position")) - .collect(Collectors.toList()); - - assertTrue(positions.contains("before_model"), - "Expected 'before_model' in agentDef.callbacks positions. Found: " + positions - + ". COUNTERFACTUAL: onModelStart not detected as override."); - assertTrue(positions.contains("after_model"), - "Expected 'after_model' in agentDef.callbacks positions. Found: " + positions - + ". COUNTERFACTUAL: onModelEnd not detected as override."); + assertNotNull(callbacks, "agentDef has no 'callbacks' key — ModelLifecycleHandler serialization broken"); + assertFalse(callbacks.isEmpty(), "agentDef.callbacks is empty — no model callbacks serialized"); + + List positions = + callbacks.stream().map(c -> (String) c.get("position")).collect(Collectors.toList()); + + assertTrue( + positions.contains("before_model"), + "Expected 'before_model' in agentDef.callbacks positions. Found: " + positions + + ". COUNTERFACTUAL: onModelStart not detected as override."); + assertTrue( + positions.contains("after_model"), + "Expected 'after_model' in agentDef.callbacks positions. Found: " + positions + + ". COUNTERFACTUAL: onModelEnd not detected as override."); } /** @@ -215,35 +235,37 @@ void test_model_callbacks_compile() { @SuppressWarnings("unchecked") void test_all_hooks_compile() { Agent agent = Agent.builder() - .name("e2e_java_all_hooks_compile") - .model(MODEL) - .instructions("You are a test agent.") - .callbacks(new AllHooksHandler()) - .build(); + .name("e2e_java_all_hooks_compile") + .model(MODEL) + .instructions("You are a test agent.") + .callbacks(new AllHooksHandler()) + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); List> callbacks = (List>) agentDef.get("callbacks"); - assertNotNull(callbacks, - "agentDef has no 'callbacks' key — AllHooksHandler serialization broken"); - - List positions = callbacks.stream() - .map(c -> (String) c.get("position")) - .collect(Collectors.toList()); - - for (String expected : List.of("before_agent", "after_agent", - "before_model", "after_model", - "before_tool", "after_tool")) { - assertTrue(positions.contains(expected), - "Expected '" + expected + "' in agentDef.callbacks positions. Found: " + positions - + ". COUNTERFACTUAL: if the override detection fails for this hook, " - + "the position won't appear."); + assertNotNull(callbacks, "agentDef has no 'callbacks' key — AllHooksHandler serialization broken"); + + List positions = + callbacks.stream().map(c -> (String) c.get("position")).collect(Collectors.toList()); + + for (String expected : List.of( + "before_agent", "after_agent", + "before_model", "after_model", + "before_tool", "after_tool")) { + assertTrue( + positions.contains(expected), + "Expected '" + expected + "' in agentDef.callbacks positions. Found: " + positions + + ". COUNTERFACTUAL: if the override detection fails for this hook, " + + "the position won't appear."); } - assertEquals(6, positions.size(), - "Expected exactly 6 callback positions, got " + positions.size() - + ". Positions: " + positions - + ". COUNTERFACTUAL: if duplicate positions are emitted or some are missing, count != 6."); + assertEquals( + 6, + positions.size(), + "Expected exactly 6 callback positions, got " + positions.size() + + ". Positions: " + positions + + ". COUNTERFACTUAL: if duplicate positions are emitted or some are missing, count != 6."); } /** @@ -258,20 +280,22 @@ void test_all_hooks_compile() { @Timeout(value = 300, unit = TimeUnit.SECONDS) void test_agent_callbacks_dont_block_execution() { Agent agent = Agent.builder() - .name("e2e_java_agent_cb_runtime") - .model(MODEL) - .instructions("Say hello in one sentence.") - .callbacks(new AgentLifecycleHandler()) - .maxTurns(2) - .build(); + .name("e2e_java_agent_cb_runtime") + .model(MODEL) + .instructions("Say hello in one sentence.") + .callbacks(new AgentLifecycleHandler()) + .maxTurns(2) + .build(); AgentResult result = runtime.run(agent, "Say hello."); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Agent with agent-lifecycle callbacks should complete normally. " - + "Status: " + result.getStatus() - + ". Error: " + result.getError() - + ". COUNTERFACTUAL: if callbacks block execution, status != COMPLETED."); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent with agent-lifecycle callbacks should complete normally. " + + "Status: " + result.getStatus() + + ". Error: " + result.getError() + + ". COUNTERFACTUAL: if callbacks block execution, status != COMPLETED."); } /** @@ -286,20 +310,22 @@ void test_agent_callbacks_dont_block_execution() { @Timeout(value = 300, unit = TimeUnit.SECONDS) void test_model_callbacks_dont_block_execution() { Agent agent = Agent.builder() - .name("e2e_java_model_cb_runtime") - .model(MODEL) - .instructions("Say hello in one sentence.") - .callbacks(new ModelLifecycleHandler()) - .maxTurns(2) - .build(); + .name("e2e_java_model_cb_runtime") + .model(MODEL) + .instructions("Say hello in one sentence.") + .callbacks(new ModelLifecycleHandler()) + .maxTurns(2) + .build(); AgentResult result = runtime.run(agent, "Say hello."); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Agent with model-lifecycle callbacks should complete normally. " - + "Status: " + result.getStatus() - + ". Error: " + result.getError() - + ". COUNTERFACTUAL: if callbacks block execution, status != COMPLETED."); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent with model-lifecycle callbacks should complete normally. " + + "Status: " + result.getStatus() + + ". Error: " + result.getError() + + ". COUNTERFACTUAL: if callbacks block execution, status != COMPLETED."); } /** @@ -313,19 +339,21 @@ void test_model_callbacks_dont_block_execution() { @Timeout(value = 300, unit = TimeUnit.SECONDS) void test_all_callbacks_dont_block_execution() { Agent agent = Agent.builder() - .name("e2e_java_all_cb_runtime") - .model(MODEL) - .instructions("Say hello in one sentence.") - .callbacks(new AllHooksHandler()) - .maxTurns(2) - .build(); + .name("e2e_java_all_cb_runtime") + .model(MODEL) + .instructions("Say hello in one sentence.") + .callbacks(new AllHooksHandler()) + .maxTurns(2) + .build(); AgentResult result = runtime.run(agent, "Say hello."); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Agent with all 6 callback hooks registered should complete normally. " - + "Status: " + result.getStatus() - + ". Error: " + result.getError() - + ". COUNTERFACTUAL: if any callback hook combination blocks, status != COMPLETED."); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent with all 6 callback hooks registered should complete normally. " + + "Status: " + result.getStatus() + + ". Error: " + result.getError() + + ". COUNTERFACTUAL: if any callback hook combination blocks, status != COMPLETED."); } } diff --git a/sdk/java/e2e/Suite14StatefulDomain.java b/sdk/java/e2e/Suite14StatefulDomain.java index 853bb29ef..944b7bac8 100644 --- a/sdk/java/e2e/Suite14StatefulDomain.java +++ b/sdk/java/e2e/Suite14StatefulDomain.java @@ -1,14 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; -import ai.agentspan.AgentConfig; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.HashSet; import java.util.List; @@ -17,7 +10,14 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.conductoross.conductor.ai.model.ToolDef; +import org.junit.jupiter.api.*; /** * Suite 12: Stateful domain propagation — structural plan() assertions. @@ -44,7 +44,7 @@ class Suite14StatefulDomain extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -57,24 +57,24 @@ static void teardown() { /** Build a minimal worker ToolDef with the given name. */ private static ToolDef workerTool(String name) { return ToolDef.builder() - .name(name) - .description("A test worker tool.") - .inputSchema(Map.of("type", "object", "properties", Map.of())) - .toolType("worker") - .func(input -> input) - .build(); + .name(name) + .description("A test worker tool.") + .inputSchema(Map.of("type", "object", "properties", Map.of())) + .toolType("worker") + .func(input -> input) + .build(); } /** Build a minimal worker ToolDef marked stateful=true. */ private static ToolDef statefulWorkerTool(String name) { return ToolDef.builder() - .name(name) - .description("A stateful test worker tool.") - .inputSchema(Map.of("type", "object", "properties", Map.of())) - .toolType("worker") - .func(input -> input) - .stateful(true) - .build(); + .name(name) + .description("A stateful test worker tool.") + .inputSchema(Map.of("type", "object", "properties", Map.of())) + .toolType("worker") + .func(input -> input) + .stateful(true) + .build(); } // ── Tests ───────────────────────────────────────────────────────────────── @@ -90,31 +90,33 @@ private static ToolDef statefulWorkerTool(String name) { @SuppressWarnings("unchecked") void test_stateful_true_propagates_to_agentDef() { Agent agent = Agent.builder() - .name("e2e_s12_stateful_true") - .model(MODEL) - .instructions("A stateful agent.") - .stateful(true) - .tools(List.of(workerTool("e2e_s12_stateful_true_tool"))) - .build(); - - Map plan = runtime.plan(agent); + .name("e2e_s12_stateful_true") + .model(MODEL) + .instructions("A stateful agent.") + .stateful(true) + .tools(List.of(workerTool("e2e_s12_stateful_true_tool"))) + .build(); + + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); List> tools = (List>) agentDef.get("tools"); assertNotNull(tools, "agentDef.tools is null. COUNTERFACTUAL: agent has a tool so tools must not be null."); Map tool = tools.stream() - .filter(t -> "e2e_s12_stateful_true_tool".equals(t.get("name"))) - .findFirst() - .orElseGet(() -> { - fail("Tool 'e2e_s12_stateful_true_tool' not found in agentDef.tools: " - + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); - return null; - }); - - assertEquals(Boolean.TRUE, tool.get("stateful"), - "tool.stateful should be true when agent is stateful(true) but got: " + tool.get("stateful") - + ". COUNTERFACTUAL: Agent.stateful(true) must propagate stateful=true to each tool in the plan."); + .filter(t -> "e2e_s12_stateful_true_tool".equals(t.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("Tool 'e2e_s12_stateful_true_tool' not found in agentDef.tools: " + + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); + return null; + }); + + assertEquals( + Boolean.TRUE, + tool.get("stateful"), + "tool.stateful should be true when agent is stateful(true) but got: " + tool.get("stateful") + + ". COUNTERFACTUAL: Agent.stateful(true) must propagate stateful=true to each tool in the plan."); } /** @@ -128,32 +130,34 @@ void test_stateful_true_propagates_to_agentDef() { @SuppressWarnings("unchecked") void test_stateful_false_default_does_not_propagate() { Agent agent = Agent.builder() - .name("e2e_s12_stateful_false") - .model(MODEL) - .instructions("A non-stateful agent (default).") - // stateful not set — defaults to false - .tools(List.of(workerTool("e2e_s12_stateful_false_tool"))) - .build(); - - Map plan = runtime.plan(agent); + .name("e2e_s12_stateful_false") + .model(MODEL) + .instructions("A non-stateful agent (default).") + // stateful not set — defaults to false + .tools(List.of(workerTool("e2e_s12_stateful_false_tool"))) + .build(); + + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); List> tools = (List>) agentDef.get("tools"); assertNotNull(tools, "agentDef.tools is null."); Map tool = tools.stream() - .filter(t -> "e2e_s12_stateful_false_tool".equals(t.get("name"))) - .findFirst() - .orElseGet(() -> { - fail("Tool 'e2e_s12_stateful_false_tool' not found."); - return null; - }); + .filter(t -> "e2e_s12_stateful_false_tool".equals(t.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("Tool 'e2e_s12_stateful_false_tool' not found."); + return null; + }); // stateful should be absent or false — not true Object statefulValue = tool.get("stateful"); - assertNotEquals(Boolean.TRUE, statefulValue, - "tool.stateful should NOT be true for a non-stateful agent (default) but got: " + statefulValue - + ". COUNTERFACTUAL: if stateful defaults to true, domain isolation is always on, which is wrong."); + assertNotEquals( + Boolean.TRUE, + statefulValue, + "tool.stateful should NOT be true for a non-stateful agent (default) but got: " + statefulValue + + ". COUNTERFACTUAL: if stateful defaults to true, domain isolation is always on, which is wrong."); } /** @@ -172,31 +176,36 @@ void test_stateful_true_agent_with_tool_inherits_domain() { ToolDef tool = workerTool("e2e_s12_domain_inherit_tool"); Agent agent = Agent.builder() - .name("e2e_s12_domain_inherit") - .model(MODEL) - .instructions("Stateful agent with a tool that must inherit the domain.") - .stateful(true) - .tools(List.of(tool)) - .build(); - - Map plan = runtime.plan(agent); + .name("e2e_s12_domain_inherit") + .model(MODEL) + .instructions("Stateful agent with a tool that must inherit the domain.") + .stateful(true) + .tools(List.of(tool)) + .build(); + + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); List> planTools = (List>) agentDef.get("tools"); assertNotNull(planTools, "agentDef.tools is null."); - assertEquals(1, planTools.size(), - "Expected 1 tool in plan but got " + planTools.size()); + assertEquals(1, planTools.size(), "Expected 1 tool in plan but got " + planTools.size()); Map planTool = planTools.get(0); - assertEquals("e2e_s12_domain_inherit_tool", planTool.get("name"), - "Tool name mismatch. Got: " + planTool.get("name")); - assertEquals("worker", planTool.get("toolType"), - "toolType should remain 'worker' even with stateful=true. Got: " + planTool.get("toolType") - + ". COUNTERFACTUAL: stateful propagation must not alter toolType."); - assertEquals(Boolean.TRUE, planTool.get("stateful"), - "tool.stateful should be true. Got: " + planTool.get("stateful") - + ". COUNTERFACTUAL: stateful flag must propagate to the tool so the worker domain is isolated."); + assertEquals( + "e2e_s12_domain_inherit_tool", + planTool.get("name"), + "Tool name mismatch. Got: " + planTool.get("name")); + assertEquals( + "worker", + planTool.get("toolType"), + "toolType should remain 'worker' even with stateful=true. Got: " + planTool.get("toolType") + + ". COUNTERFACTUAL: stateful propagation must not alter toolType."); + assertEquals( + Boolean.TRUE, + planTool.get("stateful"), + "tool.stateful should be true. Got: " + planTool.get("stateful") + + ". COUNTERFACTUAL: stateful flag must propagate to the tool so the worker domain is isolated."); } /** @@ -211,56 +220,56 @@ void test_stateful_true_agent_with_tool_inherits_domain() { @SuppressWarnings("unchecked") void test_stateful_swarm_all_subagents_inherit_flag() { Agent subAgent1 = Agent.builder() - .name("e2e_s12_swarm_sub1") - .model(MODEL) - .instructions("Swarm sub-agent 1.") - .stateful(true) - .tools(List.of(workerTool("e2e_s12_swarm_sub1_tool"))) - .build(); + .name("e2e_s12_swarm_sub1") + .model(MODEL) + .instructions("Swarm sub-agent 1.") + .stateful(true) + .tools(List.of(workerTool("e2e_s12_swarm_sub1_tool"))) + .build(); Agent subAgent2 = Agent.builder() - .name("e2e_s12_swarm_sub2") - .model(MODEL) - .instructions("Swarm sub-agent 2.") - .stateful(true) - .tools(List.of(workerTool("e2e_s12_swarm_sub2_tool"))) - .build(); + .name("e2e_s12_swarm_sub2") + .model(MODEL) + .instructions("Swarm sub-agent 2.") + .stateful(true) + .tools(List.of(workerTool("e2e_s12_swarm_sub2_tool"))) + .build(); Agent swarm = Agent.builder() - .name("e2e_s12_stateful_swarm") - .model(MODEL) - .instructions("Stateful swarm coordinator.") - .agents(subAgent1, subAgent2) - .strategy(Strategy.SWARM) - .stateful(true) - .build(); - - Map plan = runtime.plan(swarm); + .name("e2e_s12_stateful_swarm") + .model(MODEL) + .instructions("Stateful swarm coordinator.") + .agents(subAgent1, subAgent2) + .strategy(Strategy.SWARM) + .stateful(true) + .build(); + + CompileResponse plan = runtime.plan(swarm); Map agentDef = getAgentDef(plan); List> subAgents = (List>) agentDef.get("agents"); - assertNotNull(subAgents, - "agentDef.agents is null. COUNTERFACTUAL: swarm plan must include sub-agents."); - assertEquals(2, subAgents.size(), - "Expected 2 sub-agents in swarm plan but got " + subAgents.size()); + assertNotNull(subAgents, "agentDef.agents is null. COUNTERFACTUAL: swarm plan must include sub-agents."); + assertEquals(2, subAgents.size(), "Expected 2 sub-agents in swarm plan but got " + subAgents.size()); // Verify each sub-agent's tool carries stateful=true for (Map sub : subAgents) { String subName = (String) sub.get("name"); List> subTools = (List>) sub.get("tools"); - assertNotNull(subTools, - "Sub-agent '" + subName + "' has no 'tools' in plan. " - + "COUNTERFACTUAL: stateful sub-agent tools must appear in plan."); - assertFalse(subTools.isEmpty(), - "Sub-agent '" + subName + "' has empty tools list."); + assertNotNull( + subTools, + "Sub-agent '" + subName + "' has no 'tools' in plan. " + + "COUNTERFACTUAL: stateful sub-agent tools must appear in plan."); + assertFalse(subTools.isEmpty(), "Sub-agent '" + subName + "' has empty tools list."); for (Map subTool : subTools) { - assertEquals(Boolean.TRUE, subTool.get("stateful"), - "Sub-agent '" + subName + "' tool '" + subTool.get("name") - + "' stateful should be true but got: " + subTool.get("stateful") - + ". COUNTERFACTUAL: stateful=true on a swarm sub-agent must propagate to " - + "its tools so worker domain isolation works in the swarm."); + assertEquals( + Boolean.TRUE, + subTool.get("stateful"), + "Sub-agent '" + subName + "' tool '" + subTool.get("name") + + "' stateful should be true but got: " + subTool.get("stateful") + + ". COUNTERFACTUAL: stateful=true on a swarm sub-agent must propagate to " + + "its tools so worker domain isolation works in the swarm."); } } } @@ -288,52 +297,51 @@ void test_stateful_swarm_all_subagents_inherit_flag() { @SuppressWarnings("unchecked") void test_concurrent_stateful_isolation() { Agent agent1 = Agent.builder() - .name("e2e_s12_concurrent_a") - .model(MODEL) - .stateful(true) - .maxTurns(3) - .instructions("Call e2e_s12_concurrent_a_tool with input='concurrent_test'. " - + "Respond with the tool result.") - .tools(List.of(workerTool("e2e_s12_concurrent_a_tool"))) - .build(); + .name("e2e_s12_concurrent_a") + .model(MODEL) + .stateful(true) + .maxTurns(3) + .instructions("Call e2e_s12_concurrent_a_tool with input='concurrent_test'. " + + "Respond with the tool result.") + .tools(List.of(workerTool("e2e_s12_concurrent_a_tool"))) + .build(); Agent agent2 = Agent.builder() - .name("e2e_s12_concurrent_b") - .model(MODEL) - .stateful(true) - .maxTurns(3) - .instructions("Call e2e_s12_concurrent_b_tool with input='concurrent_test'. " - + "Respond with the tool result.") - .tools(List.of(workerTool("e2e_s12_concurrent_b_tool"))) - .build(); + .name("e2e_s12_concurrent_b") + .model(MODEL) + .stateful(true) + .maxTurns(3) + .instructions("Call e2e_s12_concurrent_b_tool with input='concurrent_test'. " + + "Respond with the tool result.") + .tools(List.of(workerTool("e2e_s12_concurrent_b_tool"))) + .build(); AgentResult r1; AgentResult r2; - try (AgentRuntime rt1 = new AgentRuntime(new AgentConfig(BASE_URL, null, null, 100, 1))) { + try (AgentRuntime rt1 = new AgentRuntime(new AgentConfig(100, 1))) { r1 = rt1.run(agent1, "Run 1: call the tool"); } - try (AgentRuntime rt2 = new AgentRuntime(new AgentConfig(BASE_URL, null, null, 100, 1))) { + try (AgentRuntime rt2 = new AgentRuntime(new AgentConfig(100, 1))) { r2 = rt2.run(agent2, "Run 2: call the tool"); } - assertTrue(r1.isSuccess(), - "Run 1 must succeed; status=" + r1.getStatus() + " error=" + r1.getError()); - assertTrue(r2.isSuccess(), - "Run 2 must succeed; status=" + r2.getStatus() + " error=" + r2.getError()); - assertNotEquals(r1.getExecutionId(), r2.getExecutionId(), - "Concurrent runs must have distinct execution IDs."); - - Map ttd1 = (Map) - getWorkflow(r1.getExecutionId()).getOrDefault("taskToDomain", Map.of()); - Map ttd2 = (Map) - getWorkflow(r2.getExecutionId()).getOrDefault("taskToDomain", Map.of()); - - assertFalse(ttd1.isEmpty(), - "Run 1 taskToDomain is empty — stateful agent must have a domain " - + "assignment. wfId=" + r1.getExecutionId() - + ". COUNTERFACTUAL: missing runId on start would produce an empty map."); - assertFalse(ttd2.isEmpty(), - "Run 2 taskToDomain is empty — stateful agent must have a domain " - + "assignment. wfId=" + r2.getExecutionId()); + assertTrue(r1.isSuccess(), "Run 1 must succeed; status=" + r1.getStatus() + " error=" + r1.getError()); + assertTrue(r2.isSuccess(), "Run 2 must succeed; status=" + r2.getStatus() + " error=" + r2.getError()); + assertNotEquals(r1.getExecutionId(), r2.getExecutionId(), "Concurrent runs must have distinct execution IDs."); + + Map ttd1 = + (Map) getWorkflow(r1.getExecutionId()).getOrDefault("taskToDomain", Map.of()); + Map ttd2 = + (Map) getWorkflow(r2.getExecutionId()).getOrDefault("taskToDomain", Map.of()); + + assertFalse( + ttd1.isEmpty(), + "Run 1 taskToDomain is empty — stateful agent must have a domain " + + "assignment. wfId=" + r1.getExecutionId() + + ". COUNTERFACTUAL: missing runId on start would produce an empty map."); + assertFalse( + ttd2.isEmpty(), + "Run 2 taskToDomain is empty — stateful agent must have a domain " + "assignment. wfId=" + + r2.getExecutionId()); Set domains1 = new HashSet<>(); for (Object v : ttd1.values()) if (v != null) domains1.add(v.toString()); @@ -342,10 +350,11 @@ void test_concurrent_stateful_isolation() { Set intersection = new HashSet<>(domains1); intersection.retainAll(domains2); - assertTrue(intersection.isEmpty(), - "Concurrent stateful runs must have DISJOINT domain UUIDs but overlap=" + intersection - + ". Run 1=" + domains1 + ", Run 2=" + domains2 - + ". COUNTERFACTUAL: shared domains would cause cross-execution interference."); + assertTrue( + intersection.isEmpty(), + "Concurrent stateful runs must have DISJOINT domain UUIDs but overlap=" + intersection + + ". Run 1=" + domains1 + ", Run 2=" + domains2 + + ". COUNTERFACTUAL: shared domains would cause cross-execution interference."); } /** @@ -365,33 +374,38 @@ void test_per_tool_stateful_propagates_in_plan() { ToolDef plainTool = workerTool("e2e_s12_per_tool_plain"); Agent agent = Agent.builder() - .name("e2e_s12_per_tool_agent") - .model(MODEL) - // stateful NOT set on the agent — must default to false - .tools(List.of(statefulTool, plainTool)) - .build(); + .name("e2e_s12_per_tool_agent") + .model(MODEL) + // stateful NOT set on the agent — must default to false + .tools(List.of(statefulTool, plainTool)) + .build(); - assertFalse(agent.isStateful(), - "Agent.stateful must default to false for this test to be meaningful."); + assertFalse(agent.isStateful(), "Agent.stateful must default to false for this test to be meaningful."); Map agentDef = getAgentDef(runtime.plan(agent)); List> tools = (List>) agentDef.get("tools"); assertNotNull(tools); Map statefulInPlan = tools.stream() - .filter(t -> "e2e_s12_per_tool_stateful".equals(t.get("name"))) - .findFirst().orElseThrow(); + .filter(t -> "e2e_s12_per_tool_stateful".equals(t.get("name"))) + .findFirst() + .orElseThrow(); Map plainInPlan = tools.stream() - .filter(t -> "e2e_s12_per_tool_plain".equals(t.get("name"))) - .findFirst().orElseThrow(); - - assertEquals(Boolean.TRUE, statefulInPlan.get("stateful"), - "Per-tool stateful=true MUST serialize as stateful=true even when the agent is non-stateful. " - + "Got: " + statefulInPlan.get("stateful") - + ". COUNTERFACTUAL: dropping per-tool stateful would force users to mark the whole agent stateful."); - assertNotEquals(Boolean.TRUE, plainInPlan.get("stateful"), - "Sibling non-stateful tool must NOT have stateful=true. Got: " + plainInPlan.get("stateful") - + ". COUNTERFACTUAL: blanket-setting stateful on all tools would cause unnecessary domain routing."); + .filter(t -> "e2e_s12_per_tool_plain".equals(t.get("name"))) + .findFirst() + .orElseThrow(); + + assertEquals( + Boolean.TRUE, + statefulInPlan.get("stateful"), + "Per-tool stateful=true MUST serialize as stateful=true even when the agent is non-stateful. " + + "Got: " + statefulInPlan.get("stateful") + + ". COUNTERFACTUAL: dropping per-tool stateful would force users to mark the whole agent stateful."); + assertNotEquals( + Boolean.TRUE, + plainInPlan.get("stateful"), + "Sibling non-stateful tool must NOT have stateful=true. Got: " + plainInPlan.get("stateful") + + ". COUNTERFACTUAL: blanket-setting stateful on all tools would cause unnecessary domain routing."); } /** @@ -409,34 +423,32 @@ void test_per_tool_stateful_propagates_in_plan() { @SuppressWarnings("unchecked") void test_per_tool_stateful_triggers_domain_isolation() { Agent agent1 = Agent.builder() - .name("e2e_s12_per_tool_concurrent_a") - .model(MODEL) - .maxTurns(3) - // agent NOT stateful — only the tool is - .instructions("Call e2e_s12_per_tool_concurrent_a_tool with input='x'. " - + "Respond with the tool result.") - .tools(List.of(statefulWorkerTool("e2e_s12_per_tool_concurrent_a_tool"))) - .build(); + .name("e2e_s12_per_tool_concurrent_a") + .model(MODEL) + .maxTurns(3) + // agent NOT stateful — only the tool is + .instructions( + "Call e2e_s12_per_tool_concurrent_a_tool with input='x'. " + "Respond with the tool result.") + .tools(List.of(statefulWorkerTool("e2e_s12_per_tool_concurrent_a_tool"))) + .build(); Agent agent2 = Agent.builder() - .name("e2e_s12_per_tool_concurrent_b") - .model(MODEL) - .maxTurns(3) - .instructions("Call e2e_s12_per_tool_concurrent_b_tool with input='x'. " - + "Respond with the tool result.") - .tools(List.of(statefulWorkerTool("e2e_s12_per_tool_concurrent_b_tool"))) - .build(); - - assertFalse(agent1.isStateful(), - "Pre-flight: agent1 must NOT be agent-level stateful, only the tool is."); - assertFalse(agent2.isStateful(), - "Pre-flight: agent2 must NOT be agent-level stateful, only the tool is."); + .name("e2e_s12_per_tool_concurrent_b") + .model(MODEL) + .maxTurns(3) + .instructions( + "Call e2e_s12_per_tool_concurrent_b_tool with input='x'. " + "Respond with the tool result.") + .tools(List.of(statefulWorkerTool("e2e_s12_per_tool_concurrent_b_tool"))) + .build(); + + assertFalse(agent1.isStateful(), "Pre-flight: agent1 must NOT be agent-level stateful, only the tool is."); + assertFalse(agent2.isStateful(), "Pre-flight: agent2 must NOT be agent-level stateful, only the tool is."); AgentResult r1; AgentResult r2; - try (AgentRuntime rt1 = new AgentRuntime(new AgentConfig(BASE_URL, null, null, 100, 1))) { + try (AgentRuntime rt1 = new AgentRuntime(new AgentConfig(100, 1))) { r1 = rt1.run(agent1, "Run 1: call the tool"); } - try (AgentRuntime rt2 = new AgentRuntime(new AgentConfig(BASE_URL, null, null, 100, 1))) { + try (AgentRuntime rt2 = new AgentRuntime(new AgentConfig(100, 1))) { r2 = rt2.run(agent2, "Run 2: call the tool"); } @@ -444,16 +456,16 @@ void test_per_tool_stateful_triggers_domain_isolation() { assertTrue(r2.isSuccess(), "Run 2: " + r2.getStatus() + " " + r2.getError()); assertNotEquals(r1.getExecutionId(), r2.getExecutionId()); - Map ttd1 = (Map) - getWorkflow(r1.getExecutionId()).getOrDefault("taskToDomain", Map.of()); - Map ttd2 = (Map) - getWorkflow(r2.getExecutionId()).getOrDefault("taskToDomain", Map.of()); + Map ttd1 = + (Map) getWorkflow(r1.getExecutionId()).getOrDefault("taskToDomain", Map.of()); + Map ttd2 = + (Map) getWorkflow(r2.getExecutionId()).getOrDefault("taskToDomain", Map.of()); - assertFalse(ttd1.isEmpty(), - "Per-tool stateful MUST cause a non-empty taskToDomain even when " - + "agent.stateful=false. Empty means the SDK ignored the per-tool flag."); - assertFalse(ttd2.isEmpty(), - "Per-tool stateful MUST cause a non-empty taskToDomain for run 2 as well."); + assertFalse( + ttd1.isEmpty(), + "Per-tool stateful MUST cause a non-empty taskToDomain even when " + + "agent.stateful=false. Empty means the SDK ignored the per-tool flag."); + assertFalse(ttd2.isEmpty(), "Per-tool stateful MUST cause a non-empty taskToDomain for run 2 as well."); Set d1 = new HashSet<>(); for (Object v : ttd1.values()) if (v != null) d1.add(v.toString()); @@ -461,7 +473,7 @@ void test_per_tool_stateful_triggers_domain_isolation() { for (Object v : ttd2.values()) if (v != null) d2.add(v.toString()); Set overlap = new HashSet<>(d1); overlap.retainAll(d2); - assertTrue(overlap.isEmpty(), - "Concurrent per-tool-stateful runs must have disjoint domains. Overlap=" + overlap); + assertTrue( + overlap.isEmpty(), "Concurrent per-tool-stateful runs must have disjoint domains. Overlap=" + overlap); } } diff --git a/sdk/java/e2e/Suite15Skills.java b/sdk/java/e2e/Suite15Skills.java index a88873874..037b0850a 100644 --- a/sdk/java/e2e/Suite15Skills.java +++ b/sdk/java/e2e/Suite15Skills.java @@ -1,17 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.AgentTool; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; -import ai.agentspan.internal.AgentConfigSerializer; -import ai.agentspan.skill.Skill; -import ai.agentspan.skill.SkillLoadError; -import org.junit.jupiter.api.*; -import org.junit.jupiter.api.io.TempDir; +import static org.junit.jupiter.api.Assertions.*; import java.nio.file.Files; import java.nio.file.Path; @@ -20,7 +10,18 @@ import java.util.Map; import java.util.concurrent.TimeUnit; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.internal.AgentConfigSerializer; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.skill.Skill; +import org.conductoross.conductor.ai.skill.SkillLoadError; +import org.conductoross.conductor.ai.tools.AgentTool; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.io.TempDir; /** * Suite 17: Skills — load and structural assertions for {@link Skill}. @@ -41,7 +42,7 @@ class Suite15Skills extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -60,19 +61,19 @@ static void teardown() { */ private static void writeSkillDir(Path dir) throws Exception { String skillMd = "---\n" - + "name: test_skill_e2e_s17\n" - + "params:\n" - + " mode:\n" - + " default: fast\n" - + "---\n" - + "## Overview\n" - + "A test skill with two sub-agents and a script.\n" - + "\n" - + "## Workflow\n" - + "1. If no prior tool result is available, call the test_skill_e2e_s17__echo_args tool exactly once.\n" - + "2. Pass the original user's input as the argument.\n" - + "3. After a tool result containing ECHO_ARGS_RESULT: is available, return that exact line as the final answer.\n" - + "4. If asked to continue, do not call any tool. Return the most recent ECHO_ARGS_RESULT: line exactly.\n"; + + "name: test_skill_e2e_s17\n" + + "params:\n" + + " mode:\n" + + " default: fast\n" + + "---\n" + + "## Overview\n" + + "A test skill with two sub-agents and a script.\n" + + "\n" + + "## Workflow\n" + + "1. If no prior tool result is available, call the test_skill_e2e_s17__echo_args tool exactly once.\n" + + "2. Pass the original user's input as the argument.\n" + + "3. After a tool result containing ECHO_ARGS_RESULT: is available, return that exact line as the final answer.\n" + + "4. If asked to continue, do not call any tool. Return the most recent ECHO_ARGS_RESULT: line exactly.\n"; Files.writeString(dir.resolve("SKILL.md"), skillMd); Files.writeString(dir.resolve("alpha-agent.md"), "# Alpha Agent\nYou analyze the input.\n"); Files.writeString(dir.resolve("beta-agent.md"), "# Beta Agent\nYou summarize the analysis.\n"); @@ -84,9 +85,9 @@ private static void writeSkillDir(Path dir) throws Exception { Files.createDirectories(scriptsDir); Path echoPath = scriptsDir.resolve("echo_args.py"); String echoScript = "#!/usr/bin/env python3\n" - + "import sys\n" - + "args = ' '.join(sys.argv[1:]) if len(sys.argv) > 1 else 'no-args'\n" - + "print(f'ECHO_ARGS_RESULT:{args}')\n"; + + "import sys\n" + + "args = ' '.join(sys.argv[1:]) if len(sys.argv) > 1 else 'no-args'\n" + + "print(f'ECHO_ARGS_RESULT:{args}')\n"; Files.writeString(echoPath, echoScript); } @@ -107,48 +108,55 @@ void test_skill_loading_basic_properties(@TempDir Path tempDir) throws Exception Agent agent = Skill.skill(tempDir, MODEL); - assertEquals("test_skill_e2e_s17", agent.getName(), - "Skill agent name should come from SKILL.md frontmatter. Got: " + agent.getName()); - assertEquals("skill", agent.getFramework(), - "Skill agent framework must be 'skill'. Got: " + agent.getFramework() - + ". COUNTERFACTUAL: paired with plain-agent contrast below."); - assertEquals(MODEL, agent.getModel(), - "Skill agent model should be the provided MODEL. Got: " + agent.getModel()); + assertEquals( + "test_skill_e2e_s17", + agent.getName(), + "Skill agent name should come from SKILL.md frontmatter. Got: " + agent.getName()); + assertEquals( + "skill", + agent.getFramework(), + "Skill agent framework must be 'skill'. Got: " + agent.getFramework() + + ". COUNTERFACTUAL: paired with plain-agent contrast below."); + assertEquals( + MODEL, agent.getModel(), "Skill agent model should be the provided MODEL. Got: " + agent.getModel()); Map cfg = agent.getFrameworkConfig(); assertNotNull(cfg, "Skill frameworkConfig must not be null."); Map agentFiles = (Map) cfg.get("agentFiles"); assertNotNull(agentFiles, "frameworkConfig.agentFiles missing."); - assertTrue(agentFiles.containsKey("alpha"), - "agentFiles must contain 'alpha'. Got: " + agentFiles.keySet()); - assertTrue(agentFiles.containsKey("beta"), - "agentFiles must contain 'beta'. Got: " + agentFiles.keySet()); + assertTrue(agentFiles.containsKey("alpha"), "agentFiles must contain 'alpha'. Got: " + agentFiles.keySet()); + assertTrue(agentFiles.containsKey("beta"), "agentFiles must contain 'beta'. Got: " + agentFiles.keySet()); Map> scripts = (Map>) cfg.get("scripts"); assertNotNull(scripts, "frameworkConfig.scripts missing."); - assertTrue(scripts.containsKey("echo_args"), - "scripts must contain 'echo_args'. Got: " + scripts.keySet()); - assertEquals("python", scripts.get("echo_args").get("language"), - "echo_args.language should be 'python'. Got: " + scripts.get("echo_args").get("language") - + ". COUNTERFACTUAL: if language detection always returns 'bash', this fails."); + assertTrue(scripts.containsKey("echo_args"), "scripts must contain 'echo_args'. Got: " + scripts.keySet()); + assertEquals( + "python", + scripts.get("echo_args").get("language"), + "echo_args.language should be 'python'. Got: " + + scripts.get("echo_args").get("language") + + ". COUNTERFACTUAL: if language detection always returns 'bash', this fails."); String skillMd = (String) cfg.get("skillMd"); assertNotNull(skillMd, "skillMd missing."); - assertTrue(skillMd.contains("test_skill_e2e_s17"), - "skillMd should contain the skill name. Got: " + skillMd.substring(0, Math.min(200, skillMd.length()))); + assertTrue( + skillMd.contains("test_skill_e2e_s17"), + "skillMd should contain the skill name. Got: " + skillMd.substring(0, Math.min(200, skillMd.length()))); // Counterfactual: a plain Agent has NO framework="skill". Agent plain = Agent.builder() - .name("e2e_s17_plain") - .model(MODEL) - .instructions("Plain agent.") - .build(); - assertNull(plain.getFramework(), - "Plain Agent must have framework=null. Got: " + plain.getFramework() - + ". COUNTERFACTUAL: if framework() defaulted to 'skill', every agent would be a skill."); - assertNull(plain.getFrameworkConfig(), - "Plain Agent must have frameworkConfig=null. Got: " + plain.getFrameworkConfig()); + .name("e2e_s17_plain") + .model(MODEL) + .instructions("Plain agent.") + .build(); + assertNull( + plain.getFramework(), + "Plain Agent must have framework=null. Got: " + plain.getFramework() + + ". COUNTERFACTUAL: if framework() defaulted to 'skill', every agent would be a skill."); + assertNull( + plain.getFrameworkConfig(), + "Plain Agent must have frameworkConfig=null. Got: " + plain.getFrameworkConfig()); } /** @@ -159,12 +167,13 @@ void test_skill_loading_basic_properties(@TempDir Path tempDir) throws Exception @Test @Order(2) void test_skill_missing_md_throws(@TempDir Path emptyDir) throws Exception { - SkillLoadError ex = assertThrows(SkillLoadError.class, - () -> Skill.skill(emptyDir, MODEL), - "Skill.skill() with missing SKILL.md must throw. " - + "COUNTERFACTUAL: if validation is missing, the test would pass silently."); - assertTrue(ex.getMessage().contains("SKILL.md"), - "Error message must mention 'SKILL.md'. Got: " + ex.getMessage()); + SkillLoadError ex = assertThrows( + SkillLoadError.class, + () -> Skill.skill(emptyDir, MODEL), + "Skill.skill() with missing SKILL.md must throw. " + + "COUNTERFACTUAL: if validation is missing, the test would pass silently."); + assertTrue( + ex.getMessage().contains("SKILL.md"), "Error message must mention 'SKILL.md'. Got: " + ex.getMessage()); // Counterfactual: a valid dir must succeed. Path validDir = emptyDir.resolve("valid_sub"); @@ -172,8 +181,7 @@ void test_skill_missing_md_throws(@TempDir Path emptyDir) throws Exception { writeSkillDir(validDir); Agent ok = Skill.skill(validDir, MODEL); assertNotNull(ok, "Valid skill dir should load."); - assertEquals("skill", ok.getFramework(), - "Valid skill agent should have framework='skill'."); + assertEquals("skill", ok.getFramework(), "Valid skill agent should have framework='skill'."); } /** @@ -185,18 +193,16 @@ void test_skill_missing_md_throws(@TempDir Path emptyDir) throws Exception { @Test @Order(3) void test_skill_missing_name_throws(@TempDir Path dir) throws Exception { - String badMd = "---\n" - + "description: A skill without a name\n" - + "---\n" - + "## Body\nNo name field above.\n"; + String badMd = "---\n" + "description: A skill without a name\n" + "---\n" + "## Body\nNo name field above.\n"; Files.writeString(dir.resolve("SKILL.md"), badMd); - SkillLoadError ex = assertThrows(SkillLoadError.class, - () -> Skill.skill(dir, MODEL), - "Skill.skill() with missing 'name' field must throw. " - + "COUNTERFACTUAL: if name parsing always returns a fallback, this would pass silently."); - assertTrue(ex.getMessage().toLowerCase().contains("name"), - "Error must mention 'name'. Got: " + ex.getMessage()); + SkillLoadError ex = assertThrows( + SkillLoadError.class, + () -> Skill.skill(dir, MODEL), + "Skill.skill() with missing 'name' field must throw. " + + "COUNTERFACTUAL: if name parsing always returns a fallback, this would pass silently."); + assertTrue( + ex.getMessage().toLowerCase().contains("name"), "Error must mention 'name'. Got: " + ex.getMessage()); } /** @@ -222,44 +228,56 @@ void test_skill_serializes_to_plan(@TempDir Path tempDir) throws Exception { AgentConfigSerializer ser = new AgentConfigSerializer(); Map wire = ser.serialize(skillAgent); - assertEquals("skill", wire.get("_framework"), - "Serialized wire payload._framework should be 'skill'. Got: " + wire.get("_framework") - + ". COUNTERFACTUAL: paired with the plain-agent contrast below."); - assertEquals("test_skill_e2e_s17", wire.get("name"), - "wire.name should match the skill name. Got: " + wire.get("name")); - - assertNotNull(wire.get("skillMd"), - "wire.skillMd must be present so server compiles sub-agents. " - + "COUNTERFACTUAL: if frameworkConfig is dropped, the server can't compile the skill."); + assertEquals( + "skill", + wire.get("_framework"), + "Serialized wire payload._framework should be 'skill'. Got: " + wire.get("_framework") + + ". COUNTERFACTUAL: paired with the plain-agent contrast below."); + assertEquals( + "test_skill_e2e_s17", + wire.get("name"), + "wire.name should match the skill name. Got: " + wire.get("name")); + + assertNotNull( + wire.get("skillMd"), + "wire.skillMd must be present so server compiles sub-agents. " + + "COUNTERFACTUAL: if frameworkConfig is dropped, the server can't compile the skill."); Map wireAgentFiles = (Map) wire.get("agentFiles"); assertNotNull(wireAgentFiles, "wire.agentFiles missing."); - assertTrue(wireAgentFiles.containsKey("alpha"), - "wire agentFiles must contain 'alpha'. Got: " + wireAgentFiles.keySet()); - assertTrue(wireAgentFiles.containsKey("beta"), - "wire agentFiles must contain 'beta'. Got: " + wireAgentFiles.keySet()); + assertTrue( + wireAgentFiles.containsKey("alpha"), + "wire agentFiles must contain 'alpha'. Got: " + wireAgentFiles.keySet()); + assertTrue( + wireAgentFiles.containsKey("beta"), + "wire agentFiles must contain 'beta'. Got: " + wireAgentFiles.keySet()); // Counterfactual: a plain Agent's wire payload has NO _framework Agent plain = Agent.builder() - .name("e2e_s17_plain_plan") - .model(MODEL) - .instructions("Plain.") - .build(); + .name("e2e_s17_plain_plan") + .model(MODEL) + .instructions("Plain.") + .build(); Map plainWire = ser.serialize(plain); - assertNotEquals("skill", plainWire.get("_framework"), - "Plain wire._framework must NOT be 'skill'. Got: " + plainWire.get("_framework") - + ". COUNTERFACTUAL: this proves _framework is conditional on Skill.skill()."); - assertFalse(plainWire.containsKey("skillMd"), - "Plain wire payload must NOT carry skillMd. Got keys: " + plainWire.keySet() - + ". COUNTERFACTUAL: if skillMd were always emitted, every agent would look like a skill."); - assertFalse(plainWire.containsKey("agentFiles"), - "Plain wire payload must NOT carry agentFiles. Got keys: " + plainWire.keySet()); + assertNotEquals( + "skill", + plainWire.get("_framework"), + "Plain wire._framework must NOT be 'skill'. Got: " + plainWire.get("_framework") + + ". COUNTERFACTUAL: this proves _framework is conditional on Skill.skill()."); + assertFalse( + plainWire.containsKey("skillMd"), + "Plain wire payload must NOT carry skillMd. Got keys: " + plainWire.keySet() + + ". COUNTERFACTUAL: if skillMd were always emitted, every agent would look like a skill."); + assertFalse( + plainWire.containsKey("agentFiles"), + "Plain wire payload must NOT carry agentFiles. Got keys: " + plainWire.keySet()); // Final integration check: the server accepts the skill payload and returns a valid plan. - Map plan = runtime.plan(skillAgent); + CompileResponse plan = runtime.plan(skillAgent); assertNotNull(plan, "Skill plan() should return a non-null result."); - assertNotNull(plan.get("workflowDef"), - "Skill plan().workflowDef must be present. plan keys: " + plan.keySet() - + ". COUNTERFACTUAL: if the server rejected the skill payload, this would throw or return null."); + assertNotNull( + plan.getWorkflowDef(), + "Skill plan().workflowDef must be present. plan keys: " + "[workflowDef, requiredWorkers]" + + ". COUNTERFACTUAL: if the server rejected the skill payload, this would throw or return null."); } /** @@ -284,27 +302,32 @@ void test_load_skills_multiple_dirs(@TempDir Path parent) throws Exception { Files.writeString(notSkill.resolve("README.md"), "no SKILL.md here"); Map all = Skill.loadSkills(parent, MODEL); - assertTrue(all.containsKey("skill_one"), - "loadSkills must include 'skill_one'. Got keys: " + all.keySet()); - assertTrue(all.containsKey("skill_two"), - "loadSkills must include 'skill_two'. Got keys: " + all.keySet()); - assertFalse(all.containsKey("not_a_skill"), - "loadSkills must SKIP directories without SKILL.md. Got keys: " + all.keySet() - + ". COUNTERFACTUAL: if loadSkills loaded every subdir, 'not_a_skill' would appear."); - assertEquals("e2e_s17_one", all.get("skill_one").getName(), - "Skill name must come from SKILL.md, not directory name. Got: " + all.get("skill_one").getName()); - assertEquals("e2e_s17_two", all.get("skill_two").getName(), - "Skill name must come from SKILL.md, not directory name. Got: " + all.get("skill_two").getName()); - assertEquals("skill", all.get("skill_one").getFramework(), - "Loaded skill must have framework='skill'."); + assertTrue(all.containsKey("skill_one"), "loadSkills must include 'skill_one'. Got keys: " + all.keySet()); + assertTrue(all.containsKey("skill_two"), "loadSkills must include 'skill_two'. Got keys: " + all.keySet()); + assertFalse( + all.containsKey("not_a_skill"), + "loadSkills must SKIP directories without SKILL.md. Got keys: " + all.keySet() + + ". COUNTERFACTUAL: if loadSkills loaded every subdir, 'not_a_skill' would appear."); + assertEquals( + "e2e_s17_one", + all.get("skill_one").getName(), + "Skill name must come from SKILL.md, not directory name. Got: " + + all.get("skill_one").getName()); + assertEquals( + "e2e_s17_two", + all.get("skill_two").getName(), + "Skill name must come from SKILL.md, not directory name. Got: " + + all.get("skill_two").getName()); + assertEquals("skill", all.get("skill_one").getFramework(), "Loaded skill must have framework='skill'."); // Counterfactual: empty parent yields empty map Path emptyParent = parent.resolve("empty_parent"); Files.createDirectories(emptyParent); Map none = Skill.loadSkills(emptyParent, MODEL); - assertTrue(none.isEmpty(), - "loadSkills on an empty dir must return empty map. Got: " + none.keySet() - + ". COUNTERFACTUAL: if loadSkills synthesized phantom entries, this fails."); + assertTrue( + none.isEmpty(), + "loadSkills on an empty dir must return empty map. Got: " + none.keySet() + + ". COUNTERFACTUAL: if loadSkills synthesized phantom entries, this fails."); } /** @@ -326,24 +349,35 @@ void test_skill_script_language_detection(@TempDir Path dir) throws Exception { Agent agent = Skill.skill(dir, MODEL); Map> scriptsCfg = - (Map>) agent.getFrameworkConfig().get("scripts"); + (Map>) agent.getFrameworkConfig().get("scripts"); assertNotNull(scriptsCfg, "scripts missing in frameworkConfig"); - assertEquals("python", scriptsCfg.get("py_script").get("language"), - ".py should detect as 'python'. Got: " + scriptsCfg.get("py_script").get("language")); - assertEquals("bash", scriptsCfg.get("sh_script").get("language"), - ".sh should detect as 'bash'. Got: " + scriptsCfg.get("sh_script").get("language")); - assertEquals("node", scriptsCfg.get("js_script").get("language"), - ".js should detect as 'node'. Got: " + scriptsCfg.get("js_script").get("language")); + assertEquals( + "python", + scriptsCfg.get("py_script").get("language"), + ".py should detect as 'python'. Got: " + + scriptsCfg.get("py_script").get("language")); + assertEquals( + "bash", + scriptsCfg.get("sh_script").get("language"), + ".sh should detect as 'bash'. Got: " + + scriptsCfg.get("sh_script").get("language")); + assertEquals( + "node", + scriptsCfg.get("js_script").get("language"), + ".js should detect as 'node'. Got: " + + scriptsCfg.get("js_script").get("language")); // Counterfactual contrast: each extension yields a different language. List langs = List.of( - scriptsCfg.get("py_script").get("language"), - scriptsCfg.get("sh_script").get("language"), - scriptsCfg.get("js_script").get("language")); - assertEquals(3, langs.stream().distinct().count(), - "Three distinct extensions must yield three distinct languages. Got: " + langs - + ". COUNTERFACTUAL: if detectLanguage always returned 'bash', distinct count would be 1."); + scriptsCfg.get("py_script").get("language"), + scriptsCfg.get("sh_script").get("language"), + scriptsCfg.get("js_script").get("language")); + assertEquals( + 3, + langs.stream().distinct().count(), + "Three distinct extensions must yield three distinct languages. Got: " + langs + + ". COUNTERFACTUAL: if detectLanguage always returned 'bash', distinct count would be 1."); } /** @@ -362,29 +396,28 @@ void test_skill_script_discovery_includes_filename(@TempDir Path dir) throws Exc Agent agent = Skill.skill(dir, MODEL); Map> scripts = - (Map>) agent.getFrameworkConfig().get("scripts"); + (Map>) agent.getFrameworkConfig().get("scripts"); assertNotNull(scripts, "scripts missing in frameworkConfig"); - assertTrue(scripts.containsKey("echo_args"), - "scripts must contain 'echo_args'. Got: " + scripts.keySet()); - assertEquals("python", scripts.get("echo_args").get("language"), - ".py should detect as 'python'."); - assertEquals("echo_args.py", scripts.get("echo_args").get("filename"), - "Script entry must include the filename so the server can locate it. Got: " - + scripts.get("echo_args").get("filename") - + ". COUNTERFACTUAL: dropping filename would leave the server unable to invoke the script."); + assertTrue(scripts.containsKey("echo_args"), "scripts must contain 'echo_args'. Got: " + scripts.keySet()); + assertEquals("python", scripts.get("echo_args").get("language"), ".py should detect as 'python'."); + assertEquals( + "echo_args.py", + scripts.get("echo_args").get("filename"), + "Script entry must include the filename so the server can locate it. Got: " + + scripts.get("echo_args").get("filename") + + ". COUNTERFACTUAL: dropping filename would leave the server unable to invoke the script."); // Counterfactual: a skill with no scripts dir produces no scripts entry (or empty) Path noScripts = dir.resolveSibling("no_scripts"); Files.createDirectories(noScripts); - Files.writeString(noScripts.resolve("SKILL.md"), - "---\nname: e2e_s17_no_scripts\n---\n# body\n"); + Files.writeString(noScripts.resolve("SKILL.md"), "---\nname: e2e_s17_no_scripts\n---\n# body\n"); Agent noScriptsAgent = Skill.skill(noScripts, MODEL); - Map> noScriptsCfg = - (Map>) noScriptsAgent.getFrameworkConfig().get("scripts"); - assertTrue(noScriptsCfg == null || noScriptsCfg.isEmpty(), - "A skill with no scripts/ directory must have an empty/null scripts map. Got: " - + noScriptsCfg); + Map> noScriptsCfg = (Map>) + noScriptsAgent.getFrameworkConfig().get("scripts"); + assertTrue( + noScriptsCfg == null || noScriptsCfg.isEmpty(), + "A skill with no scripts/ directory must have an empty/null scripts map. Got: " + noScriptsCfg); } @Test @@ -393,31 +426,37 @@ void test_skill_workers_execute_scripts_and_read_resources(@TempDir Path dir) th writeSkillDir(dir); Agent agent = Skill.skill(dir, MODEL); List workers = Skill.createSkillWorkers(agent); - List workerNames = workers.stream().map(Skill.SkillWorker::getName).toList(); + List workerNames = + workers.stream().map(Skill.SkillWorker::getName).toList(); - assertTrue(workerNames.contains("test_skill_e2e_s17__echo_args"), - "Skill worker list must include script worker. Got: " + workerNames); - assertTrue(workerNames.contains("test_skill_e2e_s17__read_skill_file"), - "Skill worker list must include read_skill_file worker. Got: " + workerNames); + assertTrue( + workerNames.contains("test_skill_e2e_s17__echo_args"), + "Skill worker list must include script worker. Got: " + workerNames); + assertTrue( + workerNames.contains("test_skill_e2e_s17__read_skill_file"), + "Skill worker list must include read_skill_file worker. Got: " + workerNames); Skill.SkillWorker echo = workers.stream() - .filter(w -> w.getName().endsWith("__echo_args")) - .findFirst() - .orElseThrow(); + .filter(w -> w.getName().endsWith("__echo_args")) + .findFirst() + .orElseThrow(); Object echoResult = echo.getFunc().apply(Map.of("command", "hello world")); - assertTrue(String.valueOf(echoResult).contains("ECHO_ARGS_RESULT:hello world"), - "Script worker must execute locally and return deterministic marker. Got: " + echoResult); + assertTrue( + String.valueOf(echoResult).contains("ECHO_ARGS_RESULT:hello world"), + "Script worker must execute locally and return deterministic marker. Got: " + echoResult); Skill.SkillWorker read = workers.stream() - .filter(w -> w.getName().endsWith("__read_skill_file")) - .findFirst() - .orElseThrow(); + .filter(w -> w.getName().endsWith("__read_skill_file")) + .findFirst() + .orElseThrow(); Object guide = read.getFunc().apply(Map.of("path", "references/guide.md")); - assertTrue(String.valueOf(guide).contains("JAVA_REFERENCE_GUIDE"), - "read_skill_file worker must read allowlisted resources. Got: " + guide); + assertTrue( + String.valueOf(guide).contains("JAVA_REFERENCE_GUIDE"), + "read_skill_file worker must read allowlisted resources. Got: " + guide); Object denied = read.getFunc().apply(Map.of("path", "../SKILL.md")); - assertTrue(String.valueOf(denied).contains("ERROR:"), - "read_skill_file worker must reject paths outside the allowlist. Got: " + denied); + assertTrue( + String.valueOf(denied).contains("ERROR:"), + "read_skill_file worker must reject paths outside the allowlist. Got: " + denied); } /** @@ -436,11 +475,13 @@ void test_skill_per_sub_agent_model_override(@TempDir Path dir) throws Exception Map overrides = new HashMap<>(); overrides.put("alpha", "openai/gpt-4o"); - overrides.put("beta", "anthropic/claude-3-5-sonnet-20241022"); + overrides.put("beta", "anthropic/claude-3-5-sonnet-20241022"); Agent agent = Skill.skill(dir, MODEL, overrides); - assertEquals("skill", agent.getFramework(), - "Agent loaded via the agentModels overload must still have framework='skill'."); + assertEquals( + "skill", + agent.getFramework(), + "Agent loaded via the agentModels overload must still have framework='skill'."); Map cfg = agent.getFrameworkConfig(); assertNotNull(cfg, "frameworkConfig must be present."); @@ -448,26 +489,30 @@ void test_skill_per_sub_agent_model_override(@TempDir Path dir) throws Exception // The agentModels map should be threaded into frameworkConfig under // some key — accept either 'agentModels' or 'agent_models' to be tolerant // of naming, but require at least one of the two with the supplied values. - Map threaded = (Map) cfg.getOrDefault("agentModels", - cfg.get("agent_models")); - assertNotNull(threaded, - "agentModels map must be threaded into frameworkConfig. Got keys: " + cfg.keySet() - + ". COUNTERFACTUAL: if the overload silently dropped the map, this would be null."); - assertEquals("openai/gpt-4o", threaded.get("alpha"), - "alpha sub-agent must use the override model. Got: " + threaded.get("alpha")); - assertEquals("anthropic/claude-3-5-sonnet-20241022", threaded.get("beta"), - "beta sub-agent must use the override model. Got: " + threaded.get("beta")); + Map threaded = (Map) cfg.getOrDefault("agentModels", cfg.get("agent_models")); + assertNotNull( + threaded, + "agentModels map must be threaded into frameworkConfig. Got keys: " + cfg.keySet() + + ". COUNTERFACTUAL: if the overload silently dropped the map, this would be null."); + assertEquals( + "openai/gpt-4o", + threaded.get("alpha"), + "alpha sub-agent must use the override model. Got: " + threaded.get("alpha")); + assertEquals( + "anthropic/claude-3-5-sonnet-20241022", + threaded.get("beta"), + "beta sub-agent must use the override model. Got: " + threaded.get("beta")); // Counterfactual: default overload has no per-agent model map (or it's empty). Agent defaultAgent = Skill.skill(dir, MODEL); Map defaultCfg = defaultAgent.getFrameworkConfig(); Map defaultThreaded = - (Map) defaultCfg.getOrDefault("agentModels", - defaultCfg.get("agent_models")); - assertTrue(defaultThreaded == null || defaultThreaded.isEmpty(), - "Default Skill.skill() (no agentModels) must NOT carry a populated agentModels map. " - + "Got: " + defaultThreaded - + ". COUNTERFACTUAL: this proves the override path is actually taken when supplied."); + (Map) defaultCfg.getOrDefault("agentModels", defaultCfg.get("agent_models")); + assertTrue( + defaultThreaded == null || defaultThreaded.isEmpty(), + "Default Skill.skill() (no agentModels) must NOT carry a populated agentModels map. " + + "Got: " + defaultThreaded + + ". COUNTERFACTUAL: this proves the override path is actually taken when supplied."); } @Test @@ -480,8 +525,12 @@ void test_skill_params_and_cross_skill_refs_are_threaded(@TempDir Path parent) t Files.createDirectories(main); Files.createDirectories(child); Files.createDirectories(grandchild); - Files.writeString(main.resolve("SKILL.md"), "---\nname: main-skill\nparams:\n mode:\n default: fast\n---\n# Main\nUse the child-skill skill.\n"); - Files.writeString(child.resolve("SKILL.md"), "---\nname: child-skill\nparams:\n childMode: compact\n---\n# Child\nUse the grandchild-skill skill.\n"); + Files.writeString( + main.resolve("SKILL.md"), + "---\nname: main-skill\nparams:\n mode:\n default: fast\n---\n# Main\nUse the child-skill skill.\n"); + Files.writeString( + child.resolve("SKILL.md"), + "---\nname: child-skill\nparams:\n childMode: compact\n---\n# Child\nUse the grandchild-skill skill.\n"); Files.writeString(grandchild.resolve("SKILL.md"), "---\nname: grandchild-skill\n---\n# Grandchild\n"); Agent agent = Skill.skill(main, MODEL, null, Map.of("mode", "slow", "rounds", 2)); @@ -496,24 +545,26 @@ void test_skill_params_and_cross_skill_refs_are_threaded(@TempDir Path parent) t assertTrue(refs.containsKey("child-skill"), "crossSkillRefs must include child-skill. Got: " + refs.keySet()); Map childRef = (Map) refs.get("child-skill"); Map nestedRefs = (Map) childRef.get("crossSkillRefs"); - assertTrue(nestedRefs.containsKey("grandchild-skill"), - "child-skill crossSkillRefs must include grandchild-skill. Got: " + nestedRefs.keySet()); + assertTrue( + nestedRefs.containsKey("grandchild-skill"), + "child-skill crossSkillRefs must include grandchild-skill. Got: " + nestedRefs.keySet()); Path isolatedRoot = parent.resolve("isolated-root"); Path isolatedMain = parent.resolve("isolated-main"); Path isolatedChild = isolatedRoot.resolve("isolated-child"); Files.createDirectories(isolatedMain); Files.createDirectories(isolatedChild); - Files.writeString(isolatedMain.resolve("SKILL.md"), - "---\nname: isolated-main\n---\n# Main\nUse the isolated-child skill.\n"); - Files.writeString(isolatedChild.resolve("SKILL.md"), - "---\nname: isolated-child\n---\n# Child\n"); + Files.writeString( + isolatedMain.resolve("SKILL.md"), + "---\nname: isolated-main\n---\n# Main\nUse the isolated-child skill.\n"); + Files.writeString(isolatedChild.resolve("SKILL.md"), "---\nname: isolated-child\n---\n# Child\n"); Agent isolated = Skill.skill(isolatedMain, MODEL, null, null, List.of(isolatedRoot)); Map isolatedCfg = isolated.getFrameworkConfig(); Map isolatedRefs = (Map) isolatedCfg.get("crossSkillRefs"); - assertTrue(isolatedRefs.containsKey("isolated-child"), - "explicit searchPath must resolve isolated-child. Got: " + isolatedRefs.keySet()); + assertTrue( + isolatedRefs.containsKey("isolated-child"), + "explicit searchPath must resolve isolated-child. Got: " + isolatedRefs.keySet()); } /** @@ -536,45 +587,48 @@ void test_skill_nested_in_agent_tool_compiles(@TempDir Path dir) throws Exceptio Agent skillAgent = Skill.skill(dir, MODEL); ToolDef skillTool = AgentTool.from(skillAgent, "Run the test skill with echo_args."); Object workerNamesObj = skillTool.getConfig().get("workerNames"); - assertTrue(workerNamesObj instanceof List, - "agent_tool config must include workerNames for skill worker domain routing. Got: " - + skillTool.getConfig()); + assertTrue( + workerNamesObj instanceof List, + "agent_tool config must include workerNames for skill worker domain routing. Got: " + + skillTool.getConfig()); assertEquals( - List.of("test_skill_e2e_s17__echo_args", "test_skill_e2e_s17__read_skill_file"), - ((List) workerNamesObj).stream().sorted().toList(), - "Skill agent_tool workerNames must include script and read-file workers."); + List.of("test_skill_e2e_s17__echo_args", "test_skill_e2e_s17__read_skill_file"), + ((List) workerNamesObj).stream().sorted().toList(), + "Skill agent_tool workerNames must include script and read-file workers."); Agent parent = Agent.builder() - .name("e2e_s17_skill_in_at") - .model(MODEL) - .instructions("You have one tool: test_skill_e2e_s17. Call it once and return the result.") - .tools(List.of(skillTool)) - .build(); + .name("e2e_s17_skill_in_at") + .model(MODEL) + .instructions("You have one tool: test_skill_e2e_s17. Call it once and return the result.") + .tools(List.of(skillTool)) + .build(); - Map plan = runtime.plan(parent); + CompileResponse plan = runtime.plan(parent); assertNotNull(plan, "plan() must return a non-null result for skill-in-agent_tool parent."); - Map workflowDef = (Map) plan.get("workflowDef"); - assertNotNull(workflowDef, - "workflowDef must be present. COUNTERFACTUAL: if compilation failed, this would be null."); + Map workflowDef = plan.getWorkflowDef(); + assertNotNull( + workflowDef, "workflowDef must be present. COUNTERFACTUAL: if compilation failed, this would be null."); // Plan should reference the skill name somewhere — its sub-workflow or tool entry. String wfStr = workflowDef.toString(); - assertTrue(wfStr.contains("test_skill_e2e_s17"), - "Compiled workflow must reference the skill name 'test_skill_e2e_s17'. " - + "Plan keys: " + workflowDef.keySet() - + ". COUNTERFACTUAL: skill nested in agent_tool would not appear in plan if the SDK dropped it."); + assertTrue( + wfStr.contains("test_skill_e2e_s17"), + "Compiled workflow must reference the skill name 'test_skill_e2e_s17'. " + + "Plan keys: " + workflowDef.keySet() + + ". COUNTERFACTUAL: skill nested in agent_tool would not appear in plan if the SDK dropped it."); // Counterfactual: a plain agent without the skill tool has no skill reference in its plan. Agent plainParent = Agent.builder() - .name("e2e_s17_no_skill_at") - .model(MODEL) - .instructions("Plain.") - .build(); - Map plainPlan = runtime.plan(plainParent); - Map plainWf = (Map) plainPlan.get("workflowDef"); - assertFalse(plainWf.toString().contains("test_skill_e2e_s17"), - "A parent without the skill tool must not reference the skill name. " - + "COUNTERFACTUAL: if the skill leaked into unrelated agents, this would fail."); + .name("e2e_s17_no_skill_at") + .model(MODEL) + .instructions("Plain.") + .build(); + CompileResponse plainPlan = runtime.plan(plainParent); + Map plainWf = plainPlan.getWorkflowDef(); + assertFalse( + plainWf.toString().contains("test_skill_e2e_s17"), + "A parent without the skill tool must not reference the skill name. " + + "COUNTERFACTUAL: if the skill leaked into unrelated agents, this would fail."); } @Test @@ -585,19 +639,17 @@ void test_standalone_skill_script_runs_as_worker_tool(@TempDir Path dir) throws Agent skillAgent = Skill.skill(dir, MODEL); AgentResult result = runtime.run( - skillAgent, - "java_tool_parity. Call test_skill_e2e_s17__echo_args exactly once with " - + "java_tool_parity as the command argument, then return the tool output."); + skillAgent, + "java_tool_parity. Call test_skill_e2e_s17__echo_args exactly once with " + + "java_tool_parity as the command argument, then return the tool output."); - assertTrue(result.isSuccess(), - "Standalone skill run must complete. executionId=" + result.getExecutionId() - + " status=" + result.getStatus() + " error=" + result.getError()); + assertTrue( + result.isSuccess(), + "Standalone skill run must complete. executionId=" + result.getExecutionId() + " status=" + + result.getStatus() + " error=" + result.getError()); Map workflow = getWorkflow(result.getExecutionId()); - verifyWorkerTask( - workflow, - "test_skill_e2e_s17__echo_args", - "ECHO_ARGS_RESULT:java_tool_parity"); + verifyWorkerTask(workflow, "test_skill_e2e_s17__echo_args", "ECHO_ARGS_RESULT:java_tool_parity"); } @Test @@ -608,35 +660,35 @@ void test_agent_tool_skill_workers_with_domain(@TempDir Path dir) throws Excepti Agent skillAgent = Skill.skill(dir, MODEL); ToolDef skillTool = AgentTool.from(skillAgent, "Run the test skill with echo_args."); Agent parent = Agent.builder() - .name("e2e_s17_skill_at_domain") - .model(MODEL) - .instructions("You have one tool: test_skill_e2e_s17. Call it once with the user's request, then return the result.") - .tools(List.of(skillTool)) - .stateful(true) - .maxTurns(3) - .build(); + .name("e2e_s17_skill_at_domain") + .model(MODEL) + .instructions( + "You have one tool: test_skill_e2e_s17. Call it once with the user's request, then return the result.") + .tools(List.of(skillTool)) + .stateful(true) + .maxTurns(3) + .build(); AgentResult result = runtime.run(parent, "Echo 'java_domain_proof'"); - assertTrue(result.isSuccess(), - "Nested stateful skill run must complete. executionId=" + result.getExecutionId() - + " status=" + result.getStatus() + " error=" + result.getError()); + assertTrue( + result.isSuccess(), + "Nested stateful skill run must complete. executionId=" + result.getExecutionId() + " status=" + + result.getStatus() + " error=" + result.getError()); Map workflow = getWorkflow(result.getExecutionId()); Object tasksObj = workflow.get("tasks"); assertTrue(tasksObj instanceof List, "Parent workflow must include task list. Got: " + workflow.keySet()); - Map skillTask = ((List>) tasksObj).stream() - .filter(t -> String.valueOf(t.get("taskDefName")).contains("test_skill_e2e_s17")) - .findFirst() - .orElseThrow(() -> new AssertionError("Skill sub-workflow task not found: " + tasksObj)); + Map skillTask = ((List>) tasksObj) + .stream() + .filter(t -> String.valueOf(t.get("taskDefName")).contains("test_skill_e2e_s17")) + .findFirst() + .orElseThrow(() -> new AssertionError("Skill sub-workflow task not found: " + tasksObj)); assertEquals("COMPLETED", skillTask.get("status"), "Skill SUB_WORKFLOW task must complete."); String subWorkflowId = String.valueOf(((Map) skillTask.get("outputData")).get("subWorkflowId")); assertNotNull(subWorkflowId, "Skill SUB_WORKFLOW must expose subWorkflowId."); Map subWorkflow = getWorkflow(subWorkflowId); - verifyWorkerTask( - subWorkflow, - "test_skill_e2e_s17__echo_args", - "ECHO_ARGS_RESULT:"); + verifyWorkerTask(subWorkflow, "test_skill_e2e_s17__echo_args", "ECHO_ARGS_RESULT:"); } @SuppressWarnings("unchecked") @@ -645,18 +697,19 @@ private static void verifyWorkerTask(Map workflow, String taskNa assertTrue(tasksObj instanceof List, "Workflow must include task list. Got keys: " + workflow.keySet()); List> tasks = (List>) tasksObj; List> matches = tasks.stream() - .filter(t -> String.valueOf(t.get("taskDefName")).contains(taskName) - || String.valueOf(t.get("referenceTaskName")).contains(taskName) - || String.valueOf(t.get("taskType")).contains(taskName)) - .toList(); - assertFalse(matches.isEmpty(), taskName + " was not invoked. Task defs: " - + tasks.stream().map(t -> t.get("taskDefName")).toList()); + .filter(t -> String.valueOf(t.get("taskDefName")).contains(taskName) + || String.valueOf(t.get("referenceTaskName")).contains(taskName) + || String.valueOf(t.get("taskType")).contains(taskName)) + .toList(); + assertFalse( + matches.isEmpty(), + taskName + " was not invoked. Task defs: " + + tasks.stream().map(t -> t.get("taskDefName")).toList()); for (Map task : matches) { - assertEquals("COMPLETED", task.get("status"), - taskName + " must complete as a worker task. Task: " + task); + assertEquals("COMPLETED", task.get("status"), taskName + " must complete as a worker task. Task: " + task); } boolean markerFound = matches.stream() - .anyMatch(t -> String.valueOf(t.get("outputData")).contains(marker)); + .anyMatch(t -> String.valueOf(t.get("outputData")).contains(marker)); assertTrue(markerFound, taskName + " completed but marker '" + marker + "' was missing. Tasks: " + matches); } } diff --git a/sdk/java/e2e/Suite16Synthesize.java b/sdk/java/e2e/Suite16Synthesize.java index 6e10d6bd8..4e8c2ba9b 100644 --- a/sdk/java/e2e/Suite16Synthesize.java +++ b/sdk/java/e2e/Suite16Synthesize.java @@ -1,18 +1,19 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.junit.jupiter.api.*; /** * Suite 10: synthesize flag — structural plan() assertions. @@ -29,7 +30,7 @@ class Suite16Synthesize extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -82,18 +83,18 @@ void test_handoff_default_has_final_task() { .agents(List.of(makeSubAgent("e2e_sub_alpha"), makeSubAgent("e2e_sub_beta"))) .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); // synthesize defaults to true — must NOT appear in serialized config (we only emit when false) - assertFalse(agentDef.containsKey("synthesize"), + assertFalse( + agentDef.containsKey("synthesize"), "[default handoff] agentDef should NOT contain 'synthesize' when default. Got: " + agentDef.keySet()); @SuppressWarnings("unchecked") - Map wfDef = (Map) plan.get("workflowDef"); + Map wfDef = plan.getWorkflowDef(); long finalCount = countFinalTasks(wfDef, "e2e_synth_default_handoff"); - assertEquals(1, finalCount, - "[default handoff] expected exactly 1 _final task, got " + finalCount); + assertEquals(1, finalCount, "[default handoff] expected exactly 1 _final task, got " + finalCount); } /** @@ -113,17 +114,19 @@ void test_handoff_no_synthesize_omits_final_task() { .synthesize(false) .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); - assertEquals(Boolean.FALSE, agentDef.get("synthesize"), + assertEquals( + Boolean.FALSE, + agentDef.get("synthesize"), "[synthesize=false handoff] agentDef.synthesize must be false. Got: " + agentDef.get("synthesize")); @SuppressWarnings("unchecked") - Map wfDef = (Map) plan.get("workflowDef"); + Map wfDef = plan.getWorkflowDef(); long finalCount = countFinalTasks(wfDef, "e2e_synth_false_handoff"); - assertEquals(0, finalCount, - "[synthesize=false handoff] workflow must NOT have a _final task, got " + finalCount); + assertEquals( + 0, finalCount, "[synthesize=false handoff] workflow must NOT have a _final task, got " + finalCount); } /** @@ -143,17 +146,19 @@ void test_router_no_synthesize_omits_final_task() { .synthesize(false) .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); - assertEquals(Boolean.FALSE, agentDef.get("synthesize"), + assertEquals( + Boolean.FALSE, + agentDef.get("synthesize"), "[synthesize=false router] agentDef.synthesize must be false. Got: " + agentDef.get("synthesize")); @SuppressWarnings("unchecked") - Map wfDef = (Map) plan.get("workflowDef"); + Map wfDef = plan.getWorkflowDef(); long finalCount = countFinalTasks(wfDef, "e2e_synth_false_router"); - assertEquals(0, finalCount, - "[synthesize=false router] workflow must NOT have a _final task, got " + finalCount); + assertEquals( + 0, finalCount, "[synthesize=false router] workflow must NOT have a _final task, got " + finalCount); } /** @@ -173,16 +178,17 @@ void test_swarm_no_synthesize_omits_final_task() { .synthesize(false) .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); - assertEquals(Boolean.FALSE, agentDef.get("synthesize"), + assertEquals( + Boolean.FALSE, + agentDef.get("synthesize"), "[synthesize=false swarm] agentDef.synthesize must be false. Got: " + agentDef.get("synthesize")); @SuppressWarnings("unchecked") - Map wfDef = (Map) plan.get("workflowDef"); + Map wfDef = plan.getWorkflowDef(); long finalCount = countFinalTasks(wfDef, "e2e_synth_false_swarm"); - assertEquals(0, finalCount, - "[synthesize=false swarm] workflow must NOT have a _final task, got " + finalCount); + assertEquals(0, finalCount, "[synthesize=false swarm] workflow must NOT have a _final task, got " + finalCount); } } diff --git a/sdk/java/e2e/Suite17ConfigSerialization.java b/sdk/java/e2e/Suite17ConfigSerialization.java new file mode 100644 index 000000000..60348e0d4 --- /dev/null +++ b/sdk/java/e2e/Suite17ConfigSerialization.java @@ -0,0 +1,740 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.gate.TextGate; +import org.conductoross.conductor.ai.guardrail.LLMGuardrail; +import org.conductoross.conductor.ai.guardrail.RegexGuardrail; +import org.conductoross.conductor.ai.handoff.OnCondition; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.conductoross.conductor.ai.model.DeploymentInfo; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.openai.GPTAssistantAgent; +import org.conductoross.conductor.ai.termination.StopMessageTermination; +import org.conductoross.conductor.ai.tools.HumanTool; +import org.conductoross.conductor.ai.tools.MediaTools; +import org.conductoross.conductor.ai.tools.WaitForMessageTool; +import org.junit.jupiter.api.*; + +/** + * Suite 17: Agent config serialization — structural plan() assertions. + * + *

    Verifies that agent fields and features serialize correctly into the compiled + * agentDef via the SDK → /agent/compile → compiled-output round-trip. Covered: + * stateful, baseUrl, TextGate, before/after_agent callbacks, StopMessageTermination, + * RegexGuardrail, LLMGuardrail, OnCondition handoff, + * MediaTools, WaitForMessageTool, HumanTool, GPTAssistantAgent, deploy(), and the + * parity fields reasoningEffort / contextWindowBudget / maskedFields / memory. + * + *

    All tests use plan() — no LLM calls. COUNTERFACTUAL: each test is designed to + * fail if the corresponding field serializes incorrectly. + */ +@Tag("e2e") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class Suite17ConfigSerialization extends BaseTest { + + private static AgentRuntime runtime; + + @BeforeAll + static void setup() { + runtime = new AgentRuntime(new AgentConfig(100, 1)); + } + + @AfterAll + static void teardown() { + if (runtime != null) runtime.close(); + } + + // ── Helper ──────────────────────────────────────────────────────────── + + @SuppressWarnings("unchecked") + private Map findToolByName(Map agentDef, String name) { + List> tools = (List>) agentDef.get("tools"); + assertNotNull(tools, "agentDef has no 'tools' key"); + return tools.stream() + .filter(t -> name.equals(t.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("Tool '" + name + "' not found. Available: " + + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); + return null; + }); + } + + @SuppressWarnings("unchecked") + private Map findGuardrailByName(Map agentDef, String name) { + List> guardrails = (List>) agentDef.get("guardrails"); + assertNotNull(guardrails, "agentDef has no 'guardrails' key"); + return guardrails.stream() + .filter(g -> name.equals(g.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("Guardrail '" + name + "' not found. Available: " + + guardrails.stream() + .map(g -> (String) g.get("name")) + .collect(Collectors.toList())); + return null; + }); + } + + // ── Tests ───────────────────────────────────────────────────────────── + + /** + * stateful=true propagates stateful:true to each tool (mirrors Python SDK behaviour). + * + * COUNTERFACTUAL: if stateful is not propagated, worker domain isolation won't be set up. + */ + @Test + @Order(1) + @SuppressWarnings("unchecked") + void test_stateful_field_serialized() { + ToolDef workerTool = ToolDef.builder() + .name("e2e_java_stateful_tool") + .description("A worker tool.") + .inputSchema(Map.of("type", "object", "properties", Map.of())) + .toolType("worker") + .func(input -> input) + .build(); + + Agent agent = Agent.builder() + .name("e2e_java_stateful") + .model(MODEL) + .instructions("A stateful agent.") + .stateful(true) + .tools(List.of(workerTool)) + .build(); + + CompileResponse plan = runtime.plan(agent); + Map agentDef = getAgentDef(plan); + + List> tools = (List>) agentDef.get("tools"); + assertNotNull(tools, "agentDef.tools is null"); + + Map tool = tools.stream() + .filter(t -> "e2e_java_stateful_tool".equals(t.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("Tool 'e2e_java_stateful_tool' not found"); + return null; + }); + + assertEquals( + Boolean.TRUE, + tool.get("stateful"), + "tool.stateful should be true for a stateful agent but got: " + tool.get("stateful") + + ". COUNTERFACTUAL: Agent.stateful(true) must propagate stateful=true to each tool."); + } + + /** + * baseUrl serializes to agentDef.baseUrl. + * + * COUNTERFACTUAL: if baseUrl is not serialized, field will be absent. + */ + @Test + @Order(2) + void test_base_url_serialized() { + Agent agent = Agent.builder() + .name("e2e_java_baseurl") + .model(MODEL) + .instructions("Agent with custom base URL.") + .baseUrl("http://my-llm-proxy.internal/v1") + .build(); + + CompileResponse plan = runtime.plan(agent); + Map agentDef = getAgentDef(plan); + + assertEquals( + "http://my-llm-proxy.internal/v1", + agentDef.get("baseUrl"), + "agentDef.baseUrl should be 'http://my-llm-proxy.internal/v1' but got: " + + agentDef.get("baseUrl") + + ". COUNTERFACTUAL: Agent.baseUrl() must serialize to agentDef.baseUrl."); + } + + /** + * TextGate serializes to agentDef.gate with type/text/caseSensitive fields. + * + * COUNTERFACTUAL: if gate is not serialized, the sequential pipeline won't stop. + */ + @Test + @Order(3) + @SuppressWarnings("unchecked") + void test_text_gate_serialized() { + Agent checker = Agent.builder() + .name("e2e_java_gate_checker") + .model(MODEL) + .instructions("Check output.") + .gate(new TextGate("STOP", false)) + .build(); + + CompileResponse plan = runtime.plan(checker); + Map agentDef = getAgentDef(plan); + + assertNotNull( + agentDef.get("gate"), + "agentDef.gate is null. COUNTERFACTUAL: TextGate must serialize to agentDef.gate."); + + Map gate = (Map) agentDef.get("gate"); + assertEquals( + "text_contains", gate.get("type"), "gate.type should be 'text_contains' but got: " + gate.get("type")); + assertEquals("STOP", gate.get("text"), "gate.text should be 'STOP' but got: " + gate.get("text")); + assertEquals( + false, + gate.get("caseSensitive"), + "gate.caseSensitive should be false but got: " + gate.get("caseSensitive")); + } + + /** + * before_agent_callback serializes to agentDef.callbacks with position "before_agent". + * + * COUNTERFACTUAL: if callback is not serialized, it won't fire during execution. + */ + @Test + @Order(4) + @SuppressWarnings("unchecked") + void test_before_agent_callback_serialized() { + Agent agent = Agent.builder() + .name("e2e_java_before_cb") + .model(MODEL) + .instructions("Agent with before callback.") + .beforeAgentCallback(ctx -> ctx) + .build(); + + CompileResponse plan = runtime.plan(agent); + Map agentDef = getAgentDef(plan); + + List> callbacks = (List>) agentDef.get("callbacks"); + assertNotNull( + callbacks, + "agentDef.callbacks is null. COUNTERFACTUAL: beforeAgentCallback must produce agentDef.callbacks."); + assertFalse(callbacks.isEmpty(), "agentDef.callbacks is empty."); + + boolean hasBeforeAgent = callbacks.stream().anyMatch(cb -> "before_agent".equals(cb.get("position"))); + assertTrue( + hasBeforeAgent, + "No callback with position 'before_agent' found. Callbacks: " + callbacks + + ". COUNTERFACTUAL: beforeAgentCallback must produce position='before_agent'."); + } + + /** + * after_agent_callback serializes to agentDef.callbacks with position "after_agent". + * + * COUNTERFACTUAL: if callback is not serialized, it won't fire during execution. + */ + @Test + @Order(5) + @SuppressWarnings("unchecked") + void test_after_agent_callback_serialized() { + Agent agent = Agent.builder() + .name("e2e_java_after_cb") + .model(MODEL) + .instructions("Agent with after callback.") + .afterAgentCallback(ctx -> ctx) + .build(); + + CompileResponse plan = runtime.plan(agent); + Map agentDef = getAgentDef(plan); + + List> callbacks = (List>) agentDef.get("callbacks"); + assertNotNull( + callbacks, + "agentDef.callbacks is null. COUNTERFACTUAL: afterAgentCallback must produce agentDef.callbacks."); + + boolean hasAfterAgent = callbacks.stream().anyMatch(cb -> "after_agent".equals(cb.get("position"))); + assertTrue( + hasAfterAgent, + "No callback with position 'after_agent' found. Callbacks: " + callbacks + + ". COUNTERFACTUAL: afterAgentCallback must produce position='after_agent'."); + } + + /** + * StopMessageTermination serializes to agentDef.termination with type "stop_message". + * + * COUNTERFACTUAL: if not serialized, the termination condition won't stop the agent. + */ + @Test + @Order(6) + @SuppressWarnings("unchecked") + void test_stop_message_termination_serialized() { + Agent agent = Agent.builder() + .name("e2e_java_stop_msg_term") + .model(MODEL) + .instructions("Stop when you output DONE.") + .termination(StopMessageTermination.of("DONE")) + .build(); + + CompileResponse plan = runtime.plan(agent); + Map agentDef = getAgentDef(plan); + + assertNotNull( + agentDef.get("termination"), + "agentDef.termination is null. COUNTERFACTUAL: StopMessageTermination must serialize."); + + Map term = (Map) agentDef.get("termination"); + assertEquals( + "stop_message", + term.get("type"), + "termination.type should be 'stop_message' but got: " + term.get("type") + + ". COUNTERFACTUAL: StopMessageTermination.of() must produce type='stop_message'."); + assertEquals( + "DONE", + term.get("stopMessage"), + "termination.stopMessage should be 'DONE' but got: " + term.get("stopMessage")); + } + + /** + * RegexGuardrail serializes to a guardrail with guardrailType "regex" and patterns. + * + * COUNTERFACTUAL: if guardrailType is wrong, the server won't evaluate it as regex. + */ + @Test + @Order(7) + @SuppressWarnings("unchecked") + void test_regex_guardrail_serialized() { + GuardrailDef guard = RegexGuardrail.builder() + .name("e2e_java_regex_guard") + .patterns("[\\w.+-]+@[\\w-]+\\.[\\w.-]+") + .message("No emails allowed.") + .position(Position.OUTPUT) + .onFail(OnFail.RETRY) + .build(); + + Agent agent = Agent.builder() + .name("e2e_java_regex_guard_agent") + .model(MODEL) + .instructions("Be helpful.") + .guardrails(List.of(guard)) + .build(); + + CompileResponse plan = runtime.plan(agent); + Map agentDef = getAgentDef(plan); + Map g = findGuardrailByName(agentDef, "e2e_java_regex_guard"); + + assertEquals( + "regex", + g.get("guardrailType"), + "guardrailType should be 'regex' but got: " + g.get("guardrailType") + + ". COUNTERFACTUAL: RegexGuardrail.builder().build() must produce guardrailType='regex'."); + assertEquals( + "output", g.get("position"), "guardrail position should be 'output' but got: " + g.get("position")); + + // patterns and mode are inlined at the top level of the guardrail map (not nested under config) + assertNotNull( + g.get("patterns"), + "guardrail.patterns is null. COUNTERFACTUAL: RegexGuardrail patterns must serialize at top level."); + } + + /** + * LLMGuardrail serializes to a guardrail with guardrailType "llm", model, and policy. + * + * COUNTERFACTUAL: if guardrailType is wrong, the server won't call an LLM to evaluate. + */ + @Test + @Order(8) + @SuppressWarnings("unchecked") + void test_llm_guardrail_serialized() { + GuardrailDef guard = LLMGuardrail.builder() + .name("e2e_java_llm_guard") + .model("openai/gpt-4o-mini") + .policy("Reject any harmful content.") + .position(Position.OUTPUT) + .onFail(OnFail.RETRY) + .build(); + + Agent agent = Agent.builder() + .name("e2e_java_llm_guard_agent") + .model(MODEL) + .instructions("Be helpful.") + .guardrails(List.of(guard)) + .build(); + + CompileResponse plan = runtime.plan(agent); + Map agentDef = getAgentDef(plan); + Map g = findGuardrailByName(agentDef, "e2e_java_llm_guard"); + + assertEquals( + "llm", + g.get("guardrailType"), + "guardrailType should be 'llm' but got: " + g.get("guardrailType") + + ". COUNTERFACTUAL: LLMGuardrail.builder().build() must produce guardrailType='llm'."); + + // model and policy are inlined at the top level of the guardrail map (not nested under config) + assertEquals( + "openai/gpt-4o-mini", + g.get("model"), + "guardrail.model should be 'openai/gpt-4o-mini' but got: " + g.get("model") + + ". COUNTERFACTUAL: LLMGuardrail model must serialize at top level of guardrail map."); + assertEquals( + "Reject any harmful content.", g.get("policy"), "guardrail.policy mismatch. Got: " + g.get("policy")); + } + + /** + * OnCondition handoff serializes to agentDef.handoffs with the target agent. + * + * COUNTERFACTUAL: if handoff is not serialized, the condition-based routing won't work. + */ + @Test + @Order(9) + @SuppressWarnings("unchecked") + void test_on_condition_handoff_serialized() { + Agent supervisor = Agent.builder() + .name("e2e_java_supervisor") + .model(MODEL) + .instructions("Supervisor.") + .build(); + + Agent worker = Agent.builder() + .name("e2e_java_on_condition_worker") + .model(MODEL) + .instructions("Worker that may escalate.") + .handoffs(List.of( + new OnCondition("e2e_java_supervisor", ctx -> Boolean.TRUE.equals(ctx.get("escalate"))))) + .build(); + + Agent team = Agent.builder() + .name("e2e_java_on_condition_team") + .model(MODEL) + .instructions("Coordinate agents.") + .agents(supervisor, worker) + .strategy(Strategy.HANDOFF) + .build(); + + CompileResponse plan = runtime.plan(team); + Map agentDef = getAgentDef(plan); + + // Navigate to the worker sub-agent in the plan + List> agents = (List>) agentDef.get("agents"); + assertNotNull(agents, "agentDef has no 'agents' key"); + + Map workerDef = agents.stream() + .filter(a -> "e2e_java_on_condition_worker".equals(a.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("Worker sub-agent not found in plan agents: " + + agents.stream().map(a -> (String) a.get("name")).collect(Collectors.toList())); + return null; + }); + + List> handoffs = (List>) workerDef.get("handoffs"); + assertNotNull(handoffs, "Worker sub-agent has no 'handoffs' key. COUNTERFACTUAL: OnCondition must serialize."); + assertFalse(handoffs.isEmpty(), "Worker handoffs list is empty."); + + boolean hasSupervisor = handoffs.stream().anyMatch(h -> "e2e_java_supervisor".equals(h.get("target"))); + assertTrue( + hasSupervisor, + "No handoff to 'e2e_java_supervisor' found. Handoffs: " + handoffs + + ". COUNTERFACTUAL: OnCondition target must appear in handoffs."); + } + + /** + * MediaTools (imageTool) serializes with toolType "image". + * + * COUNTERFACTUAL: if toolType is wrong, the server won't handle media correctly. + */ + @Test + @Order(11) + void test_media_tools_serialized() { + ToolDef imageTool = MediaTools.imageTool( + "e2e_java_image_tool", "Analyze an image", "imageUrl", "URL of the image to analyze"); + ToolDef audioTool = MediaTools.audioTool( + "e2e_java_audio_tool", "Transcribe audio", "audioUrl", "URL of the audio to transcribe"); + + Agent agent = Agent.builder() + .name("e2e_java_media_agent") + .model(MODEL) + .instructions("Process media.") + .tools(List.of(imageTool, audioTool)) + .build(); + + CompileResponse plan = runtime.plan(agent); + Map agentDef = getAgentDef(plan); + + Map img = findToolByName(agentDef, "e2e_java_image_tool"); + assertEquals( + "generate_image", + img.get("toolType"), + "imageTool toolType should be 'generate_image' but got: " + img.get("toolType") + + ". COUNTERFACTUAL: MediaTools.imageTool() must serialize toolType='generate_image'."); + + Map aud = findToolByName(agentDef, "e2e_java_audio_tool"); + assertEquals( + "generate_audio", + aud.get("toolType"), + "audioTool toolType should be 'generate_audio' but got: " + aud.get("toolType") + + ". COUNTERFACTUAL: MediaTools.audioTool() must serialize toolType='generate_audio'."); + } + + /** + * WaitForMessageTool serializes with toolType "pull_workflow_messages". + * + * COUNTERFACTUAL: wrong toolType means agent won't pause for messages. + */ + @Test + @Order(12) + void test_wait_for_message_tool_serialized() { + ToolDef waitTool = WaitForMessageTool.create("e2e_java_wait_msg", "Wait for incoming messages", 3, true); + + Agent agent = Agent.builder() + .name("e2e_java_wait_msg_agent") + .model(MODEL) + .instructions("Wait for messages.") + .tools(List.of(waitTool)) + .build(); + + CompileResponse plan = runtime.plan(agent); + Map agentDef = getAgentDef(plan); + + Map tool = findToolByName(agentDef, "e2e_java_wait_msg"); + assertEquals( + "pull_workflow_messages", + tool.get("toolType"), + "WaitForMessageTool toolType should be 'pull_workflow_messages' but got: " + tool.get("toolType") + + ". COUNTERFACTUAL: WaitForMessageTool must serialize toolType='pull_workflow_messages'."); + } + + /** + * HumanTool serializes with toolType "human". + * + * COUNTERFACTUAL: wrong toolType means the human-in-the-loop pause won't trigger. + */ + @Test + @Order(13) + void test_human_tool_serialized() { + ToolDef humanTool = HumanTool.create("e2e_java_human_tool", "Ask a human for input."); + + Agent agent = Agent.builder() + .name("e2e_java_human_tool_agent") + .model(MODEL) + .instructions("Pause for human input when needed.") + .tools(List.of(humanTool)) + .build(); + + CompileResponse plan = runtime.plan(agent); + Map agentDef = getAgentDef(plan); + + Map tool = findToolByName(agentDef, "e2e_java_human_tool"); + assertEquals( + "human", + tool.get("toolType"), + "HumanTool toolType should be 'human' but got: " + tool.get("toolType") + + ". COUNTERFACTUAL: HumanTool must serialize toolType='human'."); + } + + /** + * GPTAssistantAgent.create().build() returns an Agent with metadata _agent_type="gpt_assistant". + * + * COUNTERFACTUAL: if metadata is missing, the server doesn't know it's a GPT assistant. + */ + @Test + @Order(16) + @SuppressWarnings("unchecked") + void test_gpt_assistant_agent_metadata_serialized() { + Agent agent = GPTAssistantAgent.create("e2e_java_gpt_assistant") + .model("gpt-4o") + .instructions("You are a data analyst.") + .build(); + + CompileResponse plan = runtime.plan(agent); + Map agentDef = getAgentDef(plan); + + Map metadata = (Map) agentDef.get("metadata"); + assertNotNull( + metadata, + "agentDef.metadata is null. COUNTERFACTUAL: GPTAssistantAgent must set _agent_type in metadata."); + assertEquals( + "gpt_assistant", + metadata.get("_agent_type"), + "_agent_type should be 'gpt_assistant' but got: " + metadata.get("_agent_type") + + ". COUNTERFACTUAL: GPTAssistantAgent.create().build() must set metadata._agent_type='gpt_assistant'."); + + // The agent should have a call tool registered + List> tools = (List>) agentDef.get("tools"); + assertNotNull(tools, "agentDef.tools is null"); + boolean hasCallTool = tools.stream().anyMatch(t -> ((String) t.get("name")).endsWith("_assistant_call")); + assertTrue( + hasCallTool, + "GPTAssistantAgent should have a '{name}_assistant_call' tool. Tools: " + + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); + } + + /** + * deploy() pushes agent to the server and returns DeploymentInfo without error. + * + * COUNTERFACTUAL: if deploy() throws, the test fails; if it returns null registeredName, the assertion fails. + */ + @Test + @Order(17) + void test_deploy_returns_deployment_info() { + Agent agent = Agent.builder() + .name("e2e_java_deploy_target") + .model(MODEL) + .instructions("A deployable agent.") + .build(); + + List results = runtime.deploy(agent); + + assertNotNull(results, "deploy() returned null. COUNTERFACTUAL: deploy() must return a non-null list."); + assertFalse( + results.isEmpty(), + "deploy() returned empty list. COUNTERFACTUAL: deploy() must return one DeploymentInfo per agent."); + + DeploymentInfo info = results.get(0); + assertNotNull( + info.getRegisteredName(), + "DeploymentInfo.registeredName is null. COUNTERFACTUAL: server must return the registered agent name."); + assertFalse(info.getRegisteredName().isEmpty(), "DeploymentInfo.registeredName is empty."); + assertEquals( + "e2e_java_deploy_target", + info.getAgentName(), + "DeploymentInfo.agentName should be 'e2e_java_deploy_target' but got: " + info.getAgentName()); + } + + /** + * Both before and after callbacks serialize together when both are set. + * + * COUNTERFACTUAL: if only one is serialized, execution will miss one callback. + */ + @Test + @Order(18) + @SuppressWarnings("unchecked") + void test_both_agent_callbacks_serialized() { + Agent agent = Agent.builder() + .name("e2e_java_both_callbacks") + .model(MODEL) + .instructions("Agent with both callbacks.") + .beforeAgentCallback(ctx -> ctx) + .afterAgentCallback(ctx -> ctx) + .build(); + + CompileResponse plan = runtime.plan(agent); + Map agentDef = getAgentDef(plan); + + List> callbacks = (List>) agentDef.get("callbacks"); + assertNotNull(callbacks, "agentDef.callbacks is null."); + assertEquals( + 2, + callbacks.size(), + "Should have 2 callbacks (before + after) but got " + callbacks.size() + + ". Callbacks: " + callbacks + + ". COUNTERFACTUAL: both before and after agent callbacks must serialize."); + + boolean hasBefore = callbacks.stream().anyMatch(cb -> "before_agent".equals(cb.get("position"))); + boolean hasAfter = callbacks.stream().anyMatch(cb -> "after_agent".equals(cb.get("position"))); + assertTrue(hasBefore, "No 'before_agent' callback found among: " + callbacks); + assertTrue(hasAfter, "No 'after_agent' callback found among: " + callbacks); + } + + // ── Parity fields: reasoningEffort / contextWindowBudget / maskedFields / memory ── + // + // Round-trip validation (NO LLM): build an agent with the field set, compile via + // plan() against the real server, and assert the field survives the + // SDK → /agent/compile → AgentConfig → compiled-output round-trip. Most fields echo + // in workflowDef.metadata.agentDef; maskedFields maps to WorkflowDef.maskedFields, so + // valueFromPlan() checks both locations. + + @SuppressWarnings("unchecked") + private Object valueFromPlan(CompileResponse plan, String key) { + Map agentDef = getAgentDef(plan); + if (agentDef.get(key) != null) return agentDef.get(key); + Map wf = plan.getWorkflowDef(); + return wf != null ? wf.get(key) : null; + } + + @Test + @Order(19) + void test_reasoning_effort_serialized() { + Agent agent = Agent.builder() + .name("e2e_java_reasoning_effort") + .model(MODEL) + .instructions("Reasoning agent.") + .reasoningEffort("high") + .build(); + + CompileResponse plan = runtime.plan(agent); + assertEquals( + "high", + getAgentDef(plan).get("reasoningEffort"), + "agentDef.reasoningEffort should be 'high' but got: " + + getAgentDef(plan).get("reasoningEffort") + + ". COUNTERFACTUAL: Agent.reasoningEffort() must serialize to agentDef.reasoningEffort."); + } + + @Test + @Order(20) + void test_context_window_budget_serialized() { + Agent agent = Agent.builder() + .name("e2e_java_ctx_budget") + .model(MODEL) + .instructions("Budgeted agent.") + .contextWindowBudget(8000) + .build(); + + CompileResponse plan = runtime.plan(agent); + Object budget = getAgentDef(plan).get("contextWindowBudget"); + assertNotNull(budget, "agentDef.contextWindowBudget missing. COUNTERFACTUAL: must serialize."); + assertEquals( + 8000, ((Number) budget).intValue(), "agentDef.contextWindowBudget should be 8000 but got: " + budget); + } + + @Test + @Order(21) + void test_masked_fields_serialized() { + Agent agent = Agent.builder() + .name("e2e_java_masked_fields") + .model(MODEL) + .instructions("Privacy agent.") + .maskedFields("ssn", "card_number") + .build(); + + CompileResponse plan = runtime.plan(agent); + Object masked = valueFromPlan(plan, "maskedFields"); + assertNotNull( + masked, + "maskedFields not found in agentDef or workflowDef. COUNTERFACTUAL: Agent.maskedFields() must " + + "round-trip to the compiled output (agentDef.maskedFields or WorkflowDef.maskedFields)."); + assertTrue( + masked instanceof List && ((List) masked).containsAll(List.of("ssn", "card_number")), + "maskedFields should contain [ssn, card_number] but got: " + masked); + } + + @Test + @Order(22) + @SuppressWarnings("unchecked") + void test_memory_serialized() { + org.conductoross.conductor.ai.model.ConversationMemory memory = + new org.conductoross.conductor.ai.model.ConversationMemory(20) + .addSystem("You are concise.") + .addUser("hello"); + + Agent agent = Agent.builder() + .name("e2e_java_memory") + .model(MODEL) + .instructions("Stateful chat agent.") + .memory(memory) + .build(); + + CompileResponse plan = runtime.plan(agent); + Map mem = (Map) getAgentDef(plan).get("memory"); + assertNotNull( + mem, "agentDef.memory missing. COUNTERFACTUAL: Agent.memory() must serialize to agentDef.memory."); + assertEquals( + 20, + ((Number) mem.get("maxMessages")).intValue(), + "agentDef.memory.maxMessages should be 20 but got: " + mem.get("maxMessages")); + List> msgs = (List>) mem.get("messages"); + assertNotNull(msgs, "agentDef.memory.messages missing."); + assertEquals(2, msgs.size(), "memory should carry 2 messages but got: " + msgs); + } +} diff --git a/sdk/java/e2e/Suite17NewParity.java b/sdk/java/e2e/Suite17NewParity.java deleted file mode 100644 index 4d3243745..000000000 --- a/sdk/java/e2e/Suite17NewParity.java +++ /dev/null @@ -1,690 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.ClaudeCode; -import ai.agentspan.UserProxyAgent; -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.enums.Strategy; -import ai.agentspan.gate.TextGate; -import ai.agentspan.guardrail.LLMGuardrail; -import ai.agentspan.guardrail.RegexGuardrail; -import ai.agentspan.handoff.OnCondition; -import ai.agentspan.model.DeploymentInfo; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.ToolDef; -import ai.agentspan.openai.GPTAssistantAgent; -import ai.agentspan.termination.StopMessageTermination; -import ai.agentspan.tools.HumanTool; -import ai.agentspan.tools.MediaTools; -import ai.agentspan.tools.WaitForMessageTool; -import org.junit.jupiter.api.*; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Suite 11: New parity features — structural plan() assertions. - * - *

    Tests the features added for Python parity: - * stateful, baseUrl, TextGate, before/after_agent callbacks, StopMessageTermination, - * RegexGuardrail, LLMGuardrail, OnCondition handoff, UserProxyAgent, ClaudeCode model, - * MediaTools, WaitForMessageTool, HumanTool, GPTAssistantAgent, deploy(). - * - *

    All tests use plan() — no LLM calls. COUNTERFACTUAL: each test is designed to - * fail if the corresponding feature serializes incorrectly. - */ -@Tag("e2e") -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class Suite17NewParity extends BaseTest { - - private static AgentRuntime runtime; - - @BeforeAll - static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); - } - - @AfterAll - static void teardown() { - if (runtime != null) runtime.close(); - } - - // ── Helper ──────────────────────────────────────────────────────────── - - @SuppressWarnings("unchecked") - private Map findToolByName(Map agentDef, String name) { - List> tools = (List>) agentDef.get("tools"); - assertNotNull(tools, "agentDef has no 'tools' key"); - return tools.stream() - .filter(t -> name.equals(t.get("name"))) - .findFirst() - .orElseGet(() -> { - fail("Tool '" + name + "' not found. Available: " - + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); - return null; - }); - } - - @SuppressWarnings("unchecked") - private Map findGuardrailByName(Map agentDef, String name) { - List> guardrails = (List>) agentDef.get("guardrails"); - assertNotNull(guardrails, "agentDef has no 'guardrails' key"); - return guardrails.stream() - .filter(g -> name.equals(g.get("name"))) - .findFirst() - .orElseGet(() -> { - fail("Guardrail '" + name + "' not found. Available: " - + guardrails.stream().map(g -> (String) g.get("name")).collect(Collectors.toList())); - return null; - }); - } - - // ── Tests ───────────────────────────────────────────────────────────── - - /** - * stateful=true propagates stateful:true to each tool (mirrors Python SDK behaviour). - * - * COUNTERFACTUAL: if stateful is not propagated, worker domain isolation won't be set up. - */ - @Test - @Order(1) - @SuppressWarnings("unchecked") - void test_stateful_field_serialized() { - ToolDef workerTool = ToolDef.builder() - .name("e2e_java_stateful_tool") - .description("A worker tool.") - .inputSchema(Map.of("type", "object", "properties", Map.of())) - .toolType("worker") - .func(input -> input) - .build(); - - Agent agent = Agent.builder() - .name("e2e_java_stateful") - .model(MODEL) - .instructions("A stateful agent.") - .stateful(true) - .tools(List.of(workerTool)) - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - - List> tools = (List>) agentDef.get("tools"); - assertNotNull(tools, "agentDef.tools is null"); - - Map tool = tools.stream() - .filter(t -> "e2e_java_stateful_tool".equals(t.get("name"))) - .findFirst() - .orElseGet(() -> { fail("Tool 'e2e_java_stateful_tool' not found"); return null; }); - - assertEquals(Boolean.TRUE, tool.get("stateful"), - "tool.stateful should be true for a stateful agent but got: " + tool.get("stateful") - + ". COUNTERFACTUAL: Agent.stateful(true) must propagate stateful=true to each tool."); - } - - /** - * baseUrl serializes to agentDef.baseUrl. - * - * COUNTERFACTUAL: if baseUrl is not serialized, field will be absent. - */ - @Test - @Order(2) - void test_base_url_serialized() { - Agent agent = Agent.builder() - .name("e2e_java_baseurl") - .model(MODEL) - .instructions("Agent with custom base URL.") - .baseUrl("http://my-llm-proxy.internal/v1") - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - - assertEquals("http://my-llm-proxy.internal/v1", agentDef.get("baseUrl"), - "agentDef.baseUrl should be 'http://my-llm-proxy.internal/v1' but got: " - + agentDef.get("baseUrl") - + ". COUNTERFACTUAL: Agent.baseUrl() must serialize to agentDef.baseUrl."); - } - - /** - * TextGate serializes to agentDef.gate with type/text/caseSensitive fields. - * - * COUNTERFACTUAL: if gate is not serialized, the sequential pipeline won't stop. - */ - @Test - @Order(3) - @SuppressWarnings("unchecked") - void test_text_gate_serialized() { - Agent checker = Agent.builder() - .name("e2e_java_gate_checker") - .model(MODEL) - .instructions("Check output.") - .gate(new TextGate("STOP", false)) - .build(); - - Map plan = runtime.plan(checker); - Map agentDef = getAgentDef(plan); - - assertNotNull(agentDef.get("gate"), - "agentDef.gate is null. COUNTERFACTUAL: TextGate must serialize to agentDef.gate."); - - Map gate = (Map) agentDef.get("gate"); - assertEquals("text_contains", gate.get("type"), - "gate.type should be 'text_contains' but got: " + gate.get("type")); - assertEquals("STOP", gate.get("text"), - "gate.text should be 'STOP' but got: " + gate.get("text")); - assertEquals(false, gate.get("caseSensitive"), - "gate.caseSensitive should be false but got: " + gate.get("caseSensitive")); - } - - /** - * before_agent_callback serializes to agentDef.callbacks with position "before_agent". - * - * COUNTERFACTUAL: if callback is not serialized, it won't fire during execution. - */ - @Test - @Order(4) - @SuppressWarnings("unchecked") - void test_before_agent_callback_serialized() { - Agent agent = Agent.builder() - .name("e2e_java_before_cb") - .model(MODEL) - .instructions("Agent with before callback.") - .beforeAgentCallback(ctx -> ctx) - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - - List> callbacks = (List>) agentDef.get("callbacks"); - assertNotNull(callbacks, - "agentDef.callbacks is null. COUNTERFACTUAL: beforeAgentCallback must produce agentDef.callbacks."); - assertFalse(callbacks.isEmpty(), - "agentDef.callbacks is empty."); - - boolean hasBeforeAgent = callbacks.stream() - .anyMatch(cb -> "before_agent".equals(cb.get("position"))); - assertTrue(hasBeforeAgent, - "No callback with position 'before_agent' found. Callbacks: " + callbacks - + ". COUNTERFACTUAL: beforeAgentCallback must produce position='before_agent'."); - } - - /** - * after_agent_callback serializes to agentDef.callbacks with position "after_agent". - * - * COUNTERFACTUAL: if callback is not serialized, it won't fire during execution. - */ - @Test - @Order(5) - @SuppressWarnings("unchecked") - void test_after_agent_callback_serialized() { - Agent agent = Agent.builder() - .name("e2e_java_after_cb") - .model(MODEL) - .instructions("Agent with after callback.") - .afterAgentCallback(ctx -> ctx) - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - - List> callbacks = (List>) agentDef.get("callbacks"); - assertNotNull(callbacks, - "agentDef.callbacks is null. COUNTERFACTUAL: afterAgentCallback must produce agentDef.callbacks."); - - boolean hasAfterAgent = callbacks.stream() - .anyMatch(cb -> "after_agent".equals(cb.get("position"))); - assertTrue(hasAfterAgent, - "No callback with position 'after_agent' found. Callbacks: " + callbacks - + ". COUNTERFACTUAL: afterAgentCallback must produce position='after_agent'."); - } - - /** - * StopMessageTermination serializes to agentDef.termination with type "stop_message". - * - * COUNTERFACTUAL: if not serialized, the termination condition won't stop the agent. - */ - @Test - @Order(6) - @SuppressWarnings("unchecked") - void test_stop_message_termination_serialized() { - Agent agent = Agent.builder() - .name("e2e_java_stop_msg_term") - .model(MODEL) - .instructions("Stop when you output DONE.") - .termination(StopMessageTermination.of("DONE")) - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - - assertNotNull(agentDef.get("termination"), - "agentDef.termination is null. COUNTERFACTUAL: StopMessageTermination must serialize."); - - Map term = (Map) agentDef.get("termination"); - assertEquals("stop_message", term.get("type"), - "termination.type should be 'stop_message' but got: " + term.get("type") - + ". COUNTERFACTUAL: StopMessageTermination.of() must produce type='stop_message'."); - assertEquals("DONE", term.get("stopMessage"), - "termination.stopMessage should be 'DONE' but got: " + term.get("stopMessage")); - } - - /** - * RegexGuardrail serializes to a guardrail with guardrailType "regex" and patterns. - * - * COUNTERFACTUAL: if guardrailType is wrong, the server won't evaluate it as regex. - */ - @Test - @Order(7) - @SuppressWarnings("unchecked") - void test_regex_guardrail_serialized() { - GuardrailDef guard = RegexGuardrail.builder() - .name("e2e_java_regex_guard") - .patterns("[\\w.+-]+@[\\w-]+\\.[\\w.-]+") - .message("No emails allowed.") - .position(Position.OUTPUT) - .onFail(OnFail.RETRY) - .build(); - - Agent agent = Agent.builder() - .name("e2e_java_regex_guard_agent") - .model(MODEL) - .instructions("Be helpful.") - .guardrails(List.of(guard)) - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - Map g = findGuardrailByName(agentDef, "e2e_java_regex_guard"); - - assertEquals("regex", g.get("guardrailType"), - "guardrailType should be 'regex' but got: " + g.get("guardrailType") - + ". COUNTERFACTUAL: RegexGuardrail.builder().build() must produce guardrailType='regex'."); - assertEquals("output", g.get("position"), - "guardrail position should be 'output' but got: " + g.get("position")); - - // patterns and mode are inlined at the top level of the guardrail map (not nested under config) - assertNotNull(g.get("patterns"), - "guardrail.patterns is null. COUNTERFACTUAL: RegexGuardrail patterns must serialize at top level."); - } - - /** - * LLMGuardrail serializes to a guardrail with guardrailType "llm", model, and policy. - * - * COUNTERFACTUAL: if guardrailType is wrong, the server won't call an LLM to evaluate. - */ - @Test - @Order(8) - @SuppressWarnings("unchecked") - void test_llm_guardrail_serialized() { - GuardrailDef guard = LLMGuardrail.builder() - .name("e2e_java_llm_guard") - .model("openai/gpt-4o-mini") - .policy("Reject any harmful content.") - .position(Position.OUTPUT) - .onFail(OnFail.RETRY) - .build(); - - Agent agent = Agent.builder() - .name("e2e_java_llm_guard_agent") - .model(MODEL) - .instructions("Be helpful.") - .guardrails(List.of(guard)) - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - Map g = findGuardrailByName(agentDef, "e2e_java_llm_guard"); - - assertEquals("llm", g.get("guardrailType"), - "guardrailType should be 'llm' but got: " + g.get("guardrailType") - + ". COUNTERFACTUAL: LLMGuardrail.builder().build() must produce guardrailType='llm'."); - - // model and policy are inlined at the top level of the guardrail map (not nested under config) - assertEquals("openai/gpt-4o-mini", g.get("model"), - "guardrail.model should be 'openai/gpt-4o-mini' but got: " + g.get("model") - + ". COUNTERFACTUAL: LLMGuardrail model must serialize at top level of guardrail map."); - assertEquals("Reject any harmful content.", g.get("policy"), - "guardrail.policy mismatch. Got: " + g.get("policy")); - } - - /** - * OnCondition handoff serializes to agentDef.handoffs with the target agent. - * - * COUNTERFACTUAL: if handoff is not serialized, the condition-based routing won't work. - */ - @Test - @Order(9) - @SuppressWarnings("unchecked") - void test_on_condition_handoff_serialized() { - Agent supervisor = Agent.builder() - .name("e2e_java_supervisor") - .model(MODEL) - .instructions("Supervisor.") - .build(); - - Agent worker = Agent.builder() - .name("e2e_java_on_condition_worker") - .model(MODEL) - .instructions("Worker that may escalate.") - .handoffs(List.of(new OnCondition("e2e_java_supervisor", - ctx -> Boolean.TRUE.equals(ctx.get("escalate"))))) - .build(); - - Agent team = Agent.builder() - .name("e2e_java_on_condition_team") - .model(MODEL) - .instructions("Coordinate agents.") - .agents(supervisor, worker) - .strategy(Strategy.HANDOFF) - .build(); - - Map plan = runtime.plan(team); - Map agentDef = getAgentDef(plan); - - // Navigate to the worker sub-agent in the plan - List> agents = (List>) agentDef.get("agents"); - assertNotNull(agents, "agentDef has no 'agents' key"); - - Map workerDef = agents.stream() - .filter(a -> "e2e_java_on_condition_worker".equals(a.get("name"))) - .findFirst() - .orElseGet(() -> { - fail("Worker sub-agent not found in plan agents: " - + agents.stream().map(a -> (String) a.get("name")).collect(Collectors.toList())); - return null; - }); - - List> handoffs = (List>) workerDef.get("handoffs"); - assertNotNull(handoffs, - "Worker sub-agent has no 'handoffs' key. COUNTERFACTUAL: OnCondition must serialize."); - assertFalse(handoffs.isEmpty(), "Worker handoffs list is empty."); - - boolean hasSupervisor = handoffs.stream() - .anyMatch(h -> "e2e_java_supervisor".equals(h.get("target"))); - assertTrue(hasSupervisor, - "No handoff to 'e2e_java_supervisor' found. Handoffs: " + handoffs - + ". COUNTERFACTUAL: OnCondition target must appear in handoffs."); - } - - /** - * UserProxyAgent creates an agent with metadata _agent_type == "user_proxy". - * - * COUNTERFACTUAL: if metadata is missing, the server won't treat it as a human proxy. - */ - @Test - @Order(10) - @SuppressWarnings("unchecked") - void test_user_proxy_agent_metadata_serialized() { - Agent user = UserProxyAgent.create("e2e_java_user_proxy", "ALWAYS", "Continue.", MODEL); - Agent assistant = Agent.builder() - .name("e2e_java_proxy_assistant") - .model(MODEL) - .instructions("Assist the user.") - .build(); - Agent team = Agent.builder() - .name("e2e_java_proxy_team") - .model(MODEL) - .instructions("Coordinate.") - .agents(user, assistant) - .strategy(Strategy.ROUND_ROBIN) - .build(); - - Map plan = runtime.plan(team); - Map agentDef = getAgentDef(plan); - - List> agents = (List>) agentDef.get("agents"); - assertNotNull(agents, "agentDef has no 'agents'"); - - Map userDef = agents.stream() - .filter(a -> "e2e_java_user_proxy".equals(a.get("name"))) - .findFirst() - .orElseGet(() -> { - fail("UserProxyAgent not found in agents: " - + agents.stream().map(a -> (String) a.get("name")).collect(Collectors.toList())); - return null; - }); - - Map metadata = (Map) userDef.get("metadata"); - assertNotNull(metadata, - "UserProxyAgent has no 'metadata'. COUNTERFACTUAL: UserProxyAgent must set _agent_type."); - assertEquals("user_proxy", metadata.get("_agent_type"), - "_agent_type should be 'user_proxy' but got: " + metadata.get("_agent_type") - + ". COUNTERFACTUAL: UserProxyAgent must set metadata._agent_type='user_proxy'."); - assertEquals("ALWAYS", metadata.get("_human_input_mode"), - "_human_input_mode should be 'ALWAYS' but got: " + metadata.get("_human_input_mode")); - } - - /** - * MediaTools (imageTool) serializes with toolType "image". - * - * COUNTERFACTUAL: if toolType is wrong, the server won't handle media correctly. - */ - @Test - @Order(11) - void test_media_tools_serialized() { - ToolDef imageTool = MediaTools.imageTool("e2e_java_image_tool", "Analyze an image", - "imageUrl", "URL of the image to analyze"); - ToolDef audioTool = MediaTools.audioTool("e2e_java_audio_tool", "Transcribe audio", - "audioUrl", "URL of the audio to transcribe"); - - Agent agent = Agent.builder() - .name("e2e_java_media_agent") - .model(MODEL) - .instructions("Process media.") - .tools(List.of(imageTool, audioTool)) - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - - Map img = findToolByName(agentDef, "e2e_java_image_tool"); - assertEquals("generate_image", img.get("toolType"), - "imageTool toolType should be 'generate_image' but got: " + img.get("toolType") - + ". COUNTERFACTUAL: MediaTools.imageTool() must serialize toolType='generate_image'."); - - Map aud = findToolByName(agentDef, "e2e_java_audio_tool"); - assertEquals("generate_audio", aud.get("toolType"), - "audioTool toolType should be 'generate_audio' but got: " + aud.get("toolType") - + ". COUNTERFACTUAL: MediaTools.audioTool() must serialize toolType='generate_audio'."); - } - - /** - * WaitForMessageTool serializes with toolType "pull_workflow_messages". - * - * COUNTERFACTUAL: wrong toolType means agent won't pause for messages. - */ - @Test - @Order(12) - void test_wait_for_message_tool_serialized() { - ToolDef waitTool = WaitForMessageTool.create("e2e_java_wait_msg", - "Wait for incoming messages", 3, true); - - Agent agent = Agent.builder() - .name("e2e_java_wait_msg_agent") - .model(MODEL) - .instructions("Wait for messages.") - .tools(List.of(waitTool)) - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - - Map tool = findToolByName(agentDef, "e2e_java_wait_msg"); - assertEquals("pull_workflow_messages", tool.get("toolType"), - "WaitForMessageTool toolType should be 'pull_workflow_messages' but got: " + tool.get("toolType") - + ". COUNTERFACTUAL: WaitForMessageTool must serialize toolType='pull_workflow_messages'."); - } - - /** - * HumanTool serializes with toolType "human". - * - * COUNTERFACTUAL: wrong toolType means the human-in-the-loop pause won't trigger. - */ - @Test - @Order(13) - void test_human_tool_serialized() { - ToolDef humanTool = HumanTool.create("e2e_java_human_tool", "Ask a human for input."); - - Agent agent = Agent.builder() - .name("e2e_java_human_tool_agent") - .model(MODEL) - .instructions("Pause for human input when needed.") - .tools(List.of(humanTool)) - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - - Map tool = findToolByName(agentDef, "e2e_java_human_tool"); - assertEquals("human", tool.get("toolType"), - "HumanTool toolType should be 'human' but got: " + tool.get("toolType") - + ". COUNTERFACTUAL: HumanTool must serialize toolType='human'."); - } - - /** - * ClaudeCode model string produces "claude-code/{model}" format. - * - * COUNTERFACTUAL: if the model string is wrong, the agent runs on the wrong LLM. - */ - @Test - @Order(14) - void test_claude_code_model_string() { - ClaudeCode cc = new ClaudeCode("opus", ClaudeCode.PermissionMode.ACCEPT_EDITS); - - String modelString = cc.toModelString(); - assertTrue(modelString.startsWith("claude-code/"), - "ClaudeCode model string should start with 'claude-code/' but got: " + modelString - + ". COUNTERFACTUAL: ClaudeCode.toModelString() must return 'claude-code/{model}'."); - assertTrue(modelString.contains("opus"), - "ClaudeCode model string should contain 'opus' but got: " + modelString); - } - - /** - * ClaudeCode agent serializes with the correct model string in agentDef.model. - * - * COUNTERFACTUAL: if not serialized correctly, server uses a different LLM. - */ - @Test - @Order(15) - void test_claude_code_agent_plan() { - ClaudeCode cc = new ClaudeCode("sonnet"); - Agent agent = Agent.builder() - .name("e2e_java_claude_code_agent") - .model(cc.toModelString()) - .instructions("Use Claude Code.") - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - - String model = (String) agentDef.get("model"); - assertTrue(model != null && model.startsWith("claude-code/"), - "agentDef.model should start with 'claude-code/' but got: " + model - + ". COUNTERFACTUAL: ClaudeCode.toModelString() in Agent.model() must be preserved."); - } - - /** - * GPTAssistantAgent.create().build() returns an Agent with metadata _agent_type="gpt_assistant". - * - * COUNTERFACTUAL: if metadata is missing, the server doesn't know it's a GPT assistant. - */ - @Test - @Order(16) - @SuppressWarnings("unchecked") - void test_gpt_assistant_agent_metadata_serialized() { - Agent agent = GPTAssistantAgent.create("e2e_java_gpt_assistant") - .model("gpt-4o") - .instructions("You are a data analyst.") - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - - Map metadata = (Map) agentDef.get("metadata"); - assertNotNull(metadata, - "agentDef.metadata is null. COUNTERFACTUAL: GPTAssistantAgent must set _agent_type in metadata."); - assertEquals("gpt_assistant", metadata.get("_agent_type"), - "_agent_type should be 'gpt_assistant' but got: " + metadata.get("_agent_type") - + ". COUNTERFACTUAL: GPTAssistantAgent.create().build() must set metadata._agent_type='gpt_assistant'."); - - // The agent should have a call tool registered - List> tools = (List>) agentDef.get("tools"); - assertNotNull(tools, "agentDef.tools is null"); - boolean hasCallTool = tools.stream() - .anyMatch(t -> ((String) t.get("name")).endsWith("_assistant_call")); - assertTrue(hasCallTool, - "GPTAssistantAgent should have a '{name}_assistant_call' tool. Tools: " - + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); - } - - /** - * deploy() pushes agent to the server and returns DeploymentInfo without error. - * - * COUNTERFACTUAL: if deploy() throws, the test fails; if it returns null registeredName, the assertion fails. - */ - @Test - @Order(17) - void test_deploy_returns_deployment_info() { - Agent agent = Agent.builder() - .name("e2e_java_deploy_target") - .model(MODEL) - .instructions("A deployable agent.") - .build(); - - List results = runtime.deploy(agent); - - assertNotNull(results, - "deploy() returned null. COUNTERFACTUAL: deploy() must return a non-null list."); - assertFalse(results.isEmpty(), - "deploy() returned empty list. COUNTERFACTUAL: deploy() must return one DeploymentInfo per agent."); - - DeploymentInfo info = results.get(0); - assertNotNull(info.getRegisteredName(), - "DeploymentInfo.registeredName is null. COUNTERFACTUAL: server must return the registered agent name."); - assertFalse(info.getRegisteredName().isEmpty(), - "DeploymentInfo.registeredName is empty."); - assertEquals("e2e_java_deploy_target", info.getAgentName(), - "DeploymentInfo.agentName should be 'e2e_java_deploy_target' but got: " + info.getAgentName()); - } - - /** - * Both before and after callbacks serialize together when both are set. - * - * COUNTERFACTUAL: if only one is serialized, execution will miss one callback. - */ - @Test - @Order(18) - @SuppressWarnings("unchecked") - void test_both_agent_callbacks_serialized() { - Agent agent = Agent.builder() - .name("e2e_java_both_callbacks") - .model(MODEL) - .instructions("Agent with both callbacks.") - .beforeAgentCallback(ctx -> ctx) - .afterAgentCallback(ctx -> ctx) - .build(); - - Map plan = runtime.plan(agent); - Map agentDef = getAgentDef(plan); - - List> callbacks = (List>) agentDef.get("callbacks"); - assertNotNull(callbacks, "agentDef.callbacks is null."); - assertEquals(2, callbacks.size(), - "Should have 2 callbacks (before + after) but got " + callbacks.size() - + ". Callbacks: " + callbacks - + ". COUNTERFACTUAL: both before and after agent callbacks must serialize."); - - boolean hasBefore = callbacks.stream().anyMatch(cb -> "before_agent".equals(cb.get("position"))); - boolean hasAfter = callbacks.stream().anyMatch(cb -> "after_agent".equals(cb.get("position"))); - assertTrue(hasBefore, "No 'before_agent' callback found among: " + callbacks); - assertTrue(hasAfter, "No 'after_agent' callback found among: " + callbacks); - } -} diff --git a/sdk/java/e2e/Suite18ToolTypes.java b/sdk/java/e2e/Suite18ToolTypes.java index d1de121a7..fc24f80fc 100644 --- a/sdk/java/e2e/Suite18ToolTypes.java +++ b/sdk/java/e2e/Suite18ToolTypes.java @@ -1,14 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.time.Instant; import java.time.LocalDate; @@ -16,7 +9,14 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.junit.jupiter.api.*; /** * Suite 13: Tool argument types — end-to-end coverage of the @@ -41,7 +41,7 @@ class Suite18ToolTypes extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -51,25 +51,28 @@ static void teardown() { public static class TypeTools { static final AtomicReference seenLocalDate = new AtomicReference<>(); - static final AtomicReference seenInstant = new AtomicReference<>(); - static final AtomicReference seenListDate = new AtomicReference<>(); + static final AtomicReference seenInstant = new AtomicReference<>(); + static final AtomicReference seenListDate = new AtomicReference<>(); - @Tool(name = "record_item_date", - description = "Record a single date. Pass an ISO-8601 calendar date (e.g. 2026-05-12).") + @Tool( + name = "record_item_date", + description = "Record a single date. Pass an ISO-8601 calendar date (e.g. 2026-05-12).") public String recordItemDate(LocalDate date) { seenLocalDate.set(date); return "recorded"; } - @Tool(name = "record_event_time", - description = "Record an event timestamp. Pass an ISO-8601 instant (e.g. 2026-05-12T13:45:00Z).") + @Tool( + name = "record_event_time", + description = "Record an event timestamp. Pass an ISO-8601 instant (e.g. 2026-05-12T13:45:00Z).") public String recordEventTime(Instant when) { seenInstant.set(when); return "recorded"; } - @Tool(name = "record_item_dates", - description = "Record a list of dates. Pass an array of ISO-8601 calendar dates.") + @Tool( + name = "record_item_dates", + description = "Record a list of dates. Pass an array of ISO-8601 calendar dates.") public String recordItemDates(List dates) { seenListDate.set(dates); return "recorded"; @@ -79,13 +82,14 @@ public String recordItemDates(List dates) { private AgentResult runWith(String agentName, String prompt) { TypeTools tools = new TypeTools(); Agent agent = Agent.builder() - .name(agentName) - .model(MODEL) - .instructions("You must call the specified tool with the values from the user message exactly as given. " - + "Do not answer in plain text. Do not invent values. Call exactly one tool, then stop.") - .tools(ToolRegistry.fromInstance(tools)) - .maxTurns(3) - .build(); + .name(agentName) + .model(MODEL) + .instructions( + "You must call the specified tool with the values from the user message exactly as given. " + + "Do not answer in plain text. Do not invent values. Call exactly one tool, then stop.") + .tools(ToolRegistry.fromInstance(tools)) + .maxTurns(3) + .build(); return runtime.run(agent, prompt); } @@ -93,15 +97,12 @@ private AgentResult runWith(String agentName, String prompt) { @Order(1) void test_local_date_is_local_date() { TypeTools.seenLocalDate.set(null); - AgentResult result = runWith("e2e_java_types_localdate", - "Call record_item_date with date=2026-05-12."); + AgentResult result = runWith("e2e_java_types_localdate", "Call record_item_date with date=2026-05-12."); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "agent did not complete. error=" + result.getError()); + assertEquals(AgentStatus.COMPLETED, result.getStatus(), "agent did not complete. error=" + result.getError()); Object seen = TypeTools.seenLocalDate.get(); assertNotNull(seen, "tool reported called but seen reference is null — race or coercion swallow"); - assertInstanceOf(LocalDate.class, seen, - "LocalDate parameter received as " + seen.getClass()); + assertInstanceOf(LocalDate.class, seen, "LocalDate parameter received as " + seen.getClass()); assertEquals(LocalDate.of(2026, 5, 12), seen); } @@ -109,33 +110,31 @@ void test_local_date_is_local_date() { @Order(2) void test_instant_is_instant() { TypeTools.seenInstant.set(null); - AgentResult result = runWith("e2e_java_types_instant", - "Call record_event_time with when=2026-05-12T13:45:00Z."); + AgentResult result = + runWith("e2e_java_types_instant", "Call record_event_time with when=2026-05-12T13:45:00Z."); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "agent did not complete. error=" + result.getError()); + assertEquals(AgentStatus.COMPLETED, result.getStatus(), "agent did not complete. error=" + result.getError()); Object seen = TypeTools.seenInstant.get(); assertNotNull(seen); - assertInstanceOf(Instant.class, seen, - "Instant parameter received as " + seen.getClass()); + assertInstanceOf(Instant.class, seen, "Instant parameter received as " + seen.getClass()); } @Test @Order(3) void test_list_of_local_date_elements_are_local_dates() { TypeTools.seenListDate.set(null); - AgentResult result = runWith("e2e_java_types_listdate", - "Call record_item_dates with dates=[\"2026-05-12\", \"2026-05-13\"]."); + AgentResult result = runWith( + "e2e_java_types_listdate", "Call record_item_dates with dates=[\"2026-05-12\", \"2026-05-13\"]."); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "agent did not complete. error=" + result.getError()); + assertEquals(AgentStatus.COMPLETED, result.getStatus(), "agent did not complete. error=" + result.getError()); Object seen = TypeTools.seenListDate.get(); assertNotNull(seen); assertInstanceOf(List.class, seen); List list = (List) seen; assertFalse(list.isEmpty(), "list is empty"); - assertInstanceOf(LocalDate.class, list.get(0), - "List elements arrived as " + list.get(0).getClass()); + assertInstanceOf( + LocalDate.class, + list.get(0), + "List elements arrived as " + list.get(0).getClass()); } - } diff --git a/sdk/java/e2e/Suite19ManualStrategy.java b/sdk/java/e2e/Suite19ManualStrategy.java new file mode 100644 index 000000000..0253750e4 --- /dev/null +++ b/sdk/java/e2e/Suite19ManualStrategy.java @@ -0,0 +1,177 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +import static org.junit.jupiter.api.Assertions.*; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.enums.EventType; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentEvent; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.AgentStream; +import org.junit.jupiter.api.*; + +/** + * Suite 19: MANUAL strategy — functional test for the human-gated agent picker. + * + *

    MANUAL was previously serialized + enumerated in plan-only tests but NEVER + * run end-to-end in any SDK. Its {@code {name}_process_selection} worker (which + * maps the human-selected agent NAME → its positional INDEX so the server's + * SWITCH can route) had zero functional coverage — the same blind spot that hid + * the dead CLI feature. This suite drives the full path: + * + *

      + *
    1. run a MANUAL coordinator with two distinguishable sub-agents
    2. + *
    3. the workflow pauses at the {@code pick_agent} HUMAN task
    4. + *
    5. we respond {@code {"selected": ""}} — the SECOND agent, so a + * broken name→index mapping (which would default to index 0 = alpha) is + * caught
    6. + *
    7. assert the SELECTED sub-agent's sub-workflow ran and the other did not
    8. + *
    + * + *

    No LLM judging: the pick is deterministic human input and validation is on + * the workflow's SUB_WORKFLOW task names. + * + * COUNTERFACTUAL: if the process_selection worker is not registered, that SIMPLE + * task never completes → the workflow never finishes → the status assertion + * fails. If the name→index mapping is wrong, alpha runs instead of beta → the + * routing assertion fails. + */ +@Tag("e2e") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Timeout(value = 300, unit = TimeUnit.SECONDS) +class Suite19ManualStrategy extends BaseTest { + + private static AgentRuntime runtime; + + private static final HttpClient HTTP = + HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + + @BeforeAll + static void setup() { + runtime = new AgentRuntime(new AgentConfig(100, 1)); + } + + @AfterAll + static void teardown() { + if (runtime != null) runtime.close(); + } + + /** POST {"selected": ""} to complete the pending pick_agent HUMAN task. */ + private void selectAgent(String executionId, String agentName) { + try { + String body = "{\"selected\":\"" + agentName + "\"}"; + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/api/agent/" + executionId + "/respond")) + .timeout(Duration.ofSeconds(10)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + HttpResponse resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString()); + assertTrue( + resp.statusCode() < 400, "respond POST failed: HTTP " + resp.statusCode() + " body=" + resp.body()); + } catch (Exception e) { + fail("Failed to POST selection for " + executionId + ": " + e.getMessage()); + } + } + + @Test + @Order(1) + @Timeout(value = 300, unit = TimeUnit.SECONDS) + @SuppressWarnings("unchecked") + void test_manual_selection_routes_to_chosen_agent() { + Agent alpha = Agent.builder() + .name("e2e_s19_alpha") + .model(MODEL) + .instructions("You are ALPHA. Always reply with exactly: ALPHA_REPLIED") + .maxTurns(1) + .build(); + Agent beta = Agent.builder() + .name("e2e_s19_beta") + .model(MODEL) + .instructions("You are BETA. Always reply with exactly: BETA_REPLIED") + .maxTurns(1) + .build(); + + Agent coordinator = Agent.builder() + .name("e2e_s19_manual") + .model(MODEL) + .instructions("A human picks which agent answers.") + .agents(alpha, beta) + .strategy(Strategy.MANUAL) + .maxTurns(1) + .build(); + + String executionId; + try (AgentStream stream = runtime.stream(coordinator, "Who should answer this?")) { + executionId = stream.getExecutionId(); + assertNotNull(executionId, "stream has no executionId"); + + int picks = 0; + for (AgentEvent event : stream) { + if (event.getType() == EventType.WAITING) { + // Pick BETA (the second agent) — exercises name→index != 0. + String target = event.getExecutionId() != null + && !event.getExecutionId().isEmpty() + ? event.getExecutionId() + : executionId; + selectAgent(target, "e2e_s19_beta"); + picks++; + assertTrue(picks <= 5, "too many pick prompts; MANUAL loop did not settle"); + } + } + + assertTrue( + picks > 0, + "Expected a WAITING event for the pick_agent HUMAN task. " + + "COUNTERFACTUAL: if MANUAL doesn't reach the human picker, no WAITING event fires."); + + AgentResult result = stream.waitForResult(180_000, 1_000); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "MANUAL workflow did not complete after selection. status=" + result.getStatus() + + " error=" + result.getError() + + ". COUNTERFACTUAL: if the process_selection worker is unregistered, that " + + "SIMPLE task hangs and the workflow never completes."); + } + + // Assert the SELECTED sub-agent (beta) ran and the other (alpha) did not. + Map workflow = getWorkflow(executionId); + List> tasks = (List>) workflow.get("tasks"); + assertNotNull(tasks, "workflow has no 'tasks'"); + + List subWorkflowRefs = tasks.stream() + .filter(t -> "SUB_WORKFLOW".equals(t.getOrDefault("taskType", ""))) + .map(t -> String.valueOf(t.getOrDefault("referenceTaskName", ""))) + .collect(Collectors.toList()); + + boolean betaRan = subWorkflowRefs.stream().anyMatch(r -> r.contains("beta")); + boolean alphaRan = subWorkflowRefs.stream().anyMatch(r -> r.contains("alpha")); + + assertTrue( + betaRan, + "Expected a SUB_WORKFLOW for the selected agent 'beta'. SUB_WORKFLOW refs: " + + subWorkflowRefs + + ". COUNTERFACTUAL: if process_selection mapped the name to the wrong index, " + + "beta's sub-workflow would never be routed to."); + assertFalse( + alphaRan, + "Did NOT expect a SUB_WORKFLOW for the unselected agent 'alpha'. SUB_WORKFLOW refs: " + + subWorkflowRefs + + ". COUNTERFACTUAL: a name→index mapping that defaults to 0 would route to alpha."); + } +} diff --git a/sdk/java/e2e/Suite1BasicValidation.java b/sdk/java/e2e/Suite1BasicValidation.java index 9e7717850..62b881455 100644 --- a/sdk/java/e2e/Suite1BasicValidation.java +++ b/sdk/java/e2e/Suite1BasicValidation.java @@ -1,26 +1,26 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. +import static org.junit.jupiter.api.Assertions.*; -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.GuardrailResult; -import ai.agentspan.tools.HttpTool; -import org.junit.jupiter.api.*; - -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.GuardrailResult; +import org.conductoross.conductor.ai.tools.HttpTool; +import org.junit.jupiter.api.*; /** * Suite 1: Basic validation — plan() structural assertions. @@ -41,7 +41,7 @@ static void setup() { // Note: checkServerHealth() from BaseTest runs before setup(). // Use BASE_URL (without /api suffix) since AgentConfig + HttpApi // already prepend /api to every path. - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -76,9 +76,7 @@ public String credTool(String input) { private List getToolNames(Map agentDef) { List> tools = (List>) agentDef.get("tools"); if (tools == null) return List.of(); - return tools.stream() - .map(t -> (String) t.get("name")) - .collect(Collectors.toList()); + return tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList()); } @SuppressWarnings("unchecked") @@ -96,9 +94,7 @@ private Map getToolTypes(Map agentDef) { private List getGuardrailNames(Map agentDef) { List> guardrails = (List>) agentDef.get("guardrails"); if (guardrails == null) return List.of(); - return guardrails.stream() - .map(g -> (String) g.get("name")) - .collect(Collectors.toList()); + return guardrails.stream().map(g -> (String) g.get("name")).collect(Collectors.toList()); } @SuppressWarnings("unchecked") @@ -118,9 +114,7 @@ private Map findGuardrailByName(Map agentDef, St private List getSubAgentNames(Map agentDef) { List> agents = (List>) agentDef.get("agents"); if (agents == null) return List.of(); - return agents.stream() - .map(a -> (String) a.get("name")) - .collect(Collectors.toList()); + return agents.stream().map(a -> (String) a.get("name")).collect(Collectors.toList()); } // ── Tests ───────────────────────────────────────────────────────────── @@ -134,32 +128,37 @@ private List getSubAgentNames(Map agentDef) { @Order(1) void test_smoke_simple_agent_plan() { Agent agent = Agent.builder() - .name("e2e_java_smoke") - .model(MODEL) - .instructions("You are a calculator.") - .tools(ToolRegistry.fromInstance(new MathAndGreetTools())) - .build(); + .name("e2e_java_smoke") + .model(MODEL) + .instructions("You are a calculator.") + .tools(ToolRegistry.fromInstance(new MathAndGreetTools())) + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); - assertTrue(plan.containsKey("workflowDef"), - "plan() result missing 'workflowDef'. Got keys: " + plan.keySet()); - assertTrue(plan.containsKey("requiredWorkers"), - "plan() result missing 'requiredWorkers'. Got keys: " + plan.keySet()); + assertTrue( + plan.getWorkflowDef() != null && !plan.getWorkflowDef().isEmpty(), + "plan() result missing 'workflowDef'. Got keys: " + "[workflowDef, requiredWorkers]"); + assertTrue( + plan.getRequiredWorkers() != null, + "plan() result missing 'requiredWorkers'. Got keys: " + "[workflowDef, requiredWorkers]"); Map agentDef = getAgentDef(plan); List toolNames = getToolNames(agentDef); - assertTrue(toolNames.contains("e2e_add"), - "Tool 'e2e_add' not found in agentDef.tools. Found: " + toolNames); - assertTrue(toolNames.contains("e2e_greet"), - "Tool 'e2e_greet' not found in agentDef.tools. Found: " + toolNames); + assertTrue(toolNames.contains("e2e_add"), "Tool 'e2e_add' not found in agentDef.tools. Found: " + toolNames); + assertTrue( + toolNames.contains("e2e_greet"), "Tool 'e2e_greet' not found in agentDef.tools. Found: " + toolNames); Map toolTypes = getToolTypes(agentDef); - assertEquals("worker", toolTypes.get("e2e_add"), - "Tool 'e2e_add' has toolType='" + toolTypes.get("e2e_add") + "', expected 'worker'"); - assertEquals("worker", toolTypes.get("e2e_greet"), - "Tool 'e2e_greet' has toolType='" + toolTypes.get("e2e_greet") + "', expected 'worker'"); + assertEquals( + "worker", + toolTypes.get("e2e_add"), + "Tool 'e2e_add' has toolType='" + toolTypes.get("e2e_add") + "', expected 'worker'"); + assertEquals( + "worker", + toolTypes.get("e2e_greet"), + "Tool 'e2e_greet' has toolType='" + toolTypes.get("e2e_greet") + "', expected 'worker'"); } /** @@ -173,62 +172,71 @@ void test_smoke_simple_agent_plan() { void test_plan_reflects_guardrails() { // Custom (function) guardrail GuardrailDef customGuardrail = GuardrailDef.builder() - .name("e2e_custom_guard") - .position(Position.INPUT) - .onFail(OnFail.RAISE) - .func(content -> GuardrailResult.pass()) - .guardrailType("custom") - .build(); + .name("e2e_custom_guard") + .position(Position.INPUT) + .onFail(OnFail.RAISE) + .func(content -> GuardrailResult.pass()) + .guardrailType("custom") + .build(); // Regex guardrail (using config map for patterns) GuardrailDef regexGuardrail = GuardrailDef.builder() - .name("e2e_regex_guard") - .position(Position.OUTPUT) - .onFail(OnFail.RETRY) - .guardrailType("regex") - .config(Map.of("patterns", List.of("BLOCKED_PATTERN"))) - .build(); + .name("e2e_regex_guard") + .position(Position.OUTPUT) + .onFail(OnFail.RETRY) + .guardrailType("regex") + .config(Map.of("patterns", List.of("BLOCKED_PATTERN"))) + .build(); Agent agent = Agent.builder() - .name("e2e_java_guardrails") - .model(MODEL) - .instructions("You are a test agent.") - .guardrails(List.of(customGuardrail, regexGuardrail)) - .build(); + .name("e2e_java_guardrails") + .model(MODEL) + .instructions("You are a test agent.") + .guardrails(List.of(customGuardrail, regexGuardrail)) + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); @SuppressWarnings("unchecked") List> guardrails = (List>) agentDef.get("guardrails"); assertNotNull(guardrails, "agentDef has no 'guardrails' key"); - assertEquals(2, guardrails.size(), - "Expected 2 guardrails in agentDef.guardrails, got " + guardrails.size() - + ". Names found: " + getGuardrailNames(agentDef)); + assertEquals( + 2, + guardrails.size(), + "Expected 2 guardrails in agentDef.guardrails, got " + guardrails.size() + ". Names found: " + + getGuardrailNames(agentDef)); // Assert custom guardrail Map custom = findGuardrailByName(agentDef, "e2e_custom_guard"); - assertEquals("custom", custom.get("guardrailType"), - "Guardrail 'e2e_custom_guard' has guardrailType='" + custom.get("guardrailType") - + "', expected 'custom'"); - assertEquals("input", custom.get("position"), - "Guardrail 'e2e_custom_guard' has position='" + custom.get("position") - + "', expected 'input'"); - assertEquals("raise", custom.get("onFail"), - "Guardrail 'e2e_custom_guard' has onFail='" + custom.get("onFail") - + "', expected 'raise'"); + assertEquals( + "custom", + custom.get("guardrailType"), + "Guardrail 'e2e_custom_guard' has guardrailType='" + custom.get("guardrailType") + + "', expected 'custom'"); + assertEquals( + "input", + custom.get("position"), + "Guardrail 'e2e_custom_guard' has position='" + custom.get("position") + "', expected 'input'"); + assertEquals( + "raise", + custom.get("onFail"), + "Guardrail 'e2e_custom_guard' has onFail='" + custom.get("onFail") + "', expected 'raise'"); // Assert regex guardrail Map regex = findGuardrailByName(agentDef, "e2e_regex_guard"); - assertEquals("regex", regex.get("guardrailType"), - "Guardrail 'e2e_regex_guard' has guardrailType='" + regex.get("guardrailType") - + "', expected 'regex'"); - assertEquals("output", regex.get("position"), - "Guardrail 'e2e_regex_guard' has position='" + regex.get("position") - + "', expected 'output'"); - assertEquals("retry", regex.get("onFail"), - "Guardrail 'e2e_regex_guard' has onFail='" + regex.get("onFail") - + "', expected 'retry'"); + assertEquals( + "regex", + regex.get("guardrailType"), + "Guardrail 'e2e_regex_guard' has guardrailType='" + regex.get("guardrailType") + "', expected 'regex'"); + assertEquals( + "output", + regex.get("position"), + "Guardrail 'e2e_regex_guard' has position='" + regex.get("position") + "', expected 'output'"); + assertEquals( + "retry", + regex.get("onFail"), + "Guardrail 'e2e_regex_guard' has onFail='" + regex.get("onFail") + "', expected 'retry'"); } /** @@ -242,46 +250,47 @@ void test_plan_reflects_guardrails() { @SuppressWarnings("unchecked") void test_plan_reflects_credentials() { // Build tool with credentials using ToolRegistry + credential override via ToolDef - ai.agentspan.model.ToolDef credTool = ai.agentspan.model.ToolDef.builder() - .name("e2e_api_cred_tool") - .description("Tool that needs API credentials") - .inputSchema(Map.of("type", "object", "properties", Map.of())) - .toolType("worker") - .credentials(List.of("E2E_API_KEY")) - .build(); + org.conductoross.conductor.ai.model.ToolDef credTool = org.conductoross.conductor.ai.model.ToolDef.builder() + .name("e2e_api_cred_tool") + .description("Tool that needs API credentials") + .inputSchema(Map.of("type", "object", "properties", Map.of())) + .toolType("worker") + .credentials(List.of("E2E_API_KEY")) + .build(); Agent agent = Agent.builder() - .name("e2e_java_creds") - .model(MODEL) - .instructions("Use tools.") - .tools(List.of(credTool)) - .build(); + .name("e2e_java_creds") + .model(MODEL) + .instructions("Use tools.") + .tools(List.of(credTool)) + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); List> tools = (List>) agentDef.get("tools"); assertNotNull(tools, "agentDef has no 'tools' key"); Map foundTool = tools.stream() - .filter(t -> "e2e_api_cred_tool".equals(t.get("name"))) - .findFirst() - .orElse(null); - assertNotNull(foundTool, - "Tool 'e2e_api_cred_tool' not found in agentDef.tools. " - + "Tool credentials not serialized to agentDef"); + .filter(t -> "e2e_api_cred_tool".equals(t.get("name"))) + .findFirst() + .orElse(null); + assertNotNull( + foundTool, + "Tool 'e2e_api_cred_tool' not found in agentDef.tools. " + + "Tool credentials not serialized to agentDef"); Map config = (Map) foundTool.get("config"); - assertNotNull(config, - "Tool 'e2e_api_cred_tool' has no 'config' key. " - + "Tool credentials not serialized to agentDef"); + assertNotNull( + config, + "Tool 'e2e_api_cred_tool' has no 'config' key. " + "Tool credentials not serialized to agentDef"); List creds = (List) config.get("credentials"); - assertNotNull(creds, - "Tool 'e2e_api_cred_tool'.config has no 'credentials'. " - + "Tool credentials not serialized to agentDef"); - assertTrue(creds.contains("E2E_API_KEY"), - "Expected 'E2E_API_KEY' in tool credentials, got: " + creds); + assertNotNull( + creds, + "Tool 'e2e_api_cred_tool'.config has no 'credentials'. " + + "Tool credentials not serialized to agentDef"); + assertTrue(creds.contains("E2E_API_KEY"), "Expected 'E2E_API_KEY' in tool credentials, got: " + creds); } /** @@ -294,39 +303,42 @@ void test_plan_reflects_credentials() { @Order(4) void test_plan_sub_agent_produces_sub_workflow() { Agent child = Agent.builder() - .name("e2e_java_child") - .model(MODEL) - .instructions("You are a helper.") - .build(); + .name("e2e_java_child") + .model(MODEL) + .instructions("You are a helper.") + .build(); Agent parent = Agent.builder() - .name("e2e_java_parent") - .model(MODEL) - .instructions("Delegate to child.") - .agents(child) - .strategy(Strategy.HANDOFF) - .build(); - - Map plan = runtime.plan(parent); + .name("e2e_java_parent") + .model(MODEL) + .instructions("Delegate to child.") + .agents(child) + .strategy(Strategy.HANDOFF) + .build(); + + CompileResponse plan = runtime.plan(parent); Map agentDef = getAgentDef(plan); List subNames = getSubAgentNames(agentDef); - assertTrue(subNames.contains("e2e_java_child"), - "Sub-agent 'e2e_java_child' not in agentDef.agents. Found: " + subNames); + assertTrue( + subNames.contains("e2e_java_child"), + "Sub-agent 'e2e_java_child' not in agentDef.agents. Found: " + subNames); - assertEquals("handoff", agentDef.get("strategy"), - "agentDef.strategy is '" + agentDef.get("strategy") + "', expected 'handoff'"); + assertEquals( + "handoff", + agentDef.get("strategy"), + "agentDef.strategy is '" + agentDef.get("strategy") + "', expected 'handoff'"); // Assert SUB_WORKFLOW task exists in the compiled workflow @SuppressWarnings("unchecked") - Map workflowDef = (Map) plan.get("workflowDef"); + Map workflowDef = plan.getWorkflowDef(); List> allTasks = allTasksFlat(workflowDef); - boolean hasSubWorkflow = allTasks.stream() - .anyMatch(t -> "SUB_WORKFLOW".equals(t.get("type"))); - assertTrue(hasSubWorkflow, - "No SUB_WORKFLOW task in compiled workflow. " - + "Task types found: " + allTasks.stream() - .map(t -> (String) t.get("type")).collect(Collectors.toSet()) - + ". An agent with sub-agents should compile to SUB_WORKFLOW tasks."); + boolean hasSubWorkflow = allTasks.stream().anyMatch(t -> "SUB_WORKFLOW".equals(t.get("type"))); + assertTrue( + hasSubWorkflow, + "No SUB_WORKFLOW task in compiled workflow. " + + "Task types found: " + + allTasks.stream().map(t -> (String) t.get("type")).collect(Collectors.toSet()) + + ". An agent with sub-agents should compile to SUB_WORKFLOW tasks."); } /** @@ -341,10 +353,10 @@ void test_plan_sub_agent_produces_sub_workflow() { void test_plan_all_8_strategies() { // Create a router agent for the ROUTER strategy Agent routerLead = Agent.builder() - .name("e2e_java_router_lead") - .model(MODEL) - .instructions("Route to the correct agent.") - .build(); + .name("e2e_java_router_lead") + .model(MODEL) + .instructions("Route to the correct agent.") + .build(); // Build 8 sub-agents with every strategy, each having 2 sub-sub-agents Strategy[] strategies = { @@ -363,14 +375,21 @@ void test_plan_all_8_strategies() { Strategy strat = strategies[i]; String stratName = strategyNames[i]; Agent.Builder b = Agent.builder() - .name("e2e_java_strat_" + stratName) - .model(MODEL) - .instructions("Agent with " + stratName + " strategy.") - .agents( - Agent.builder().name("e2e_java_" + stratName + "_s1").model(MODEL).instructions("Sub1.").build(), - Agent.builder().name("e2e_java_" + stratName + "_s2").model(MODEL).instructions("Sub2.").build() - ) - .strategy(strat); + .name("e2e_java_strat_" + stratName) + .model(MODEL) + .instructions("Agent with " + stratName + " strategy.") + .agents( + Agent.builder() + .name("e2e_java_" + stratName + "_s1") + .model(MODEL) + .instructions("Sub1.") + .build(), + Agent.builder() + .name("e2e_java_" + stratName + "_s2") + .model(MODEL) + .instructions("Sub2.") + .build()) + .strategy(strat); if (strat == Strategy.ROUTER) { b.router(routerLead); } @@ -378,14 +397,14 @@ void test_plan_all_8_strategies() { } Agent parent = Agent.builder() - .name("e2e_java_all_strategies") - .model(MODEL) - .instructions("Top-level agent with all strategies as sub-agents.") - .agents(subAgents.toArray(new Agent[0])) - .strategy(Strategy.HANDOFF) - .build(); - - Map plan = runtime.plan(parent); + .name("e2e_java_all_strategies") + .model(MODEL) + .instructions("Top-level agent with all strategies as sub-agents.") + .agents(subAgents.toArray(new Agent[0])) + .strategy(Strategy.HANDOFF) + .build(); + + CompileResponse plan = runtime.plan(parent); Map agentDef = getAgentDef(plan); List> compiledAgents = (List>) agentDef.get("agents"); @@ -398,14 +417,16 @@ void test_plan_all_8_strategies() { for (int i = 0; i < strategies.length; i++) { String stratName = strategyNames[i]; String agentName = "e2e_java_strat_" + stratName; - assertTrue(agentByName.containsKey(agentName), - "Sub-agent '" + agentName + "' not found in agentDef.agents. " - + "Found: " + agentByName.keySet()); + assertTrue( + agentByName.containsKey(agentName), + "Sub-agent '" + agentName + "' not found in agentDef.agents. " + "Found: " + agentByName.keySet()); Object actualStrategy = agentByName.get(agentName).get("strategy"); - assertEquals(stratName, actualStrategy, - "Sub-agent '" + agentName + "' has strategy='" + actualStrategy - + "', expected '" + stratName + "'. " - + "Strategy enum may not serialize to the correct JSON value."); + assertEquals( + stratName, + actualStrategy, + "Sub-agent '" + agentName + "' has strategy='" + actualStrategy + + "', expected '" + stratName + "'. " + + "Strategy enum may not serialize to the correct JSON value."); } } @@ -417,31 +438,34 @@ void test_plan_all_8_strategies() { @Test @Order(6) void test_plan_http_tool() { - ai.agentspan.model.ToolDef httpTool = HttpTool.builder() - .name("e2e_java_http_tool") - .description("Calls a remote HTTP endpoint") - .url("https://api.example.com/data") - .method("GET") - .build(); + org.conductoross.conductor.ai.model.ToolDef httpTool = HttpTool.builder() + .name("e2e_java_http_tool") + .description("Calls a remote HTTP endpoint") + .url("https://api.example.com/data") + .method("GET") + .build(); Agent agent = Agent.builder() - .name("e2e_java_http_agent") - .model(MODEL) - .instructions("Use the HTTP tool.") - .tools(List.of(httpTool)) - .build(); + .name("e2e_java_http_agent") + .model(MODEL) + .instructions("Use the HTTP tool.") + .tools(List.of(httpTool)) + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); List toolNames = getToolNames(agentDef); - assertTrue(toolNames.contains("e2e_java_http_tool"), - "HTTP tool 'e2e_java_http_tool' not found in agentDef.tools. Found: " + toolNames); + assertTrue( + toolNames.contains("e2e_java_http_tool"), + "HTTP tool 'e2e_java_http_tool' not found in agentDef.tools. Found: " + toolNames); Map toolTypes = getToolTypes(agentDef); - assertEquals("http", toolTypes.get("e2e_java_http_tool"), - "HTTP tool 'e2e_java_http_tool' has toolType='" + toolTypes.get("e2e_java_http_tool") - + "', expected 'http'. HttpTool should serialize as type 'http'."); + assertEquals( + "http", + toolTypes.get("e2e_java_http_tool"), + "HTTP tool 'e2e_java_http_tool' has toolType='" + toolTypes.get("e2e_java_http_tool") + + "', expected 'http'. HttpTool should serialize as type 'http'."); } /** @@ -453,29 +477,33 @@ void test_plan_http_tool() { @Order(7) void test_sequential_pipeline_plan() { Agent a = Agent.builder() - .name("e2e_java_seq_a") - .model(MODEL) - .instructions("First step.") - .build(); + .name("e2e_java_seq_a") + .model(MODEL) + .instructions("First step.") + .build(); Agent b = Agent.builder() - .name("e2e_java_seq_b") - .model(MODEL) - .instructions("Second step.") - .build(); + .name("e2e_java_seq_b") + .model(MODEL) + .instructions("Second step.") + .build(); Agent pipeline = a.then(b); - Map plan = runtime.plan(pipeline); + CompileResponse plan = runtime.plan(pipeline); Map agentDef = getAgentDef(plan); - assertEquals("sequential", agentDef.get("strategy"), - "Pipeline agentDef.strategy is '" + agentDef.get("strategy") - + "', expected 'sequential'. Agent.then() should produce SEQUENTIAL strategy."); + assertEquals( + "sequential", + agentDef.get("strategy"), + "Pipeline agentDef.strategy is '" + agentDef.get("strategy") + + "', expected 'sequential'. Agent.then() should produce SEQUENTIAL strategy."); List subNames = getSubAgentNames(agentDef); - assertTrue(subNames.contains("e2e_java_seq_a"), - "Agent 'e2e_java_seq_a' not in agentDef.agents. Found: " + subNames); - assertTrue(subNames.contains("e2e_java_seq_b"), - "Agent 'e2e_java_seq_b' not in agentDef.agents. Found: " + subNames); + assertTrue( + subNames.contains("e2e_java_seq_a"), + "Agent 'e2e_java_seq_a' not in agentDef.agents. Found: " + subNames); + assertTrue( + subNames.contains("e2e_java_seq_b"), + "Agent 'e2e_java_seq_b' not in agentDef.agents. Found: " + subNames); } } diff --git a/sdk/java/e2e/Suite2ToolCalling.java b/sdk/java/e2e/Suite2ToolCalling.java index 9578ddf3d..641875e7b 100644 --- a/sdk/java/e2e/Suite2ToolCalling.java +++ b/sdk/java/e2e/Suite2ToolCalling.java @@ -1,19 +1,19 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.junit.jupiter.api.*; /** * Suite 2: Tool Calling — agent execution with side-effect validation. @@ -43,7 +43,7 @@ class Suite2ToolCalling extends BaseTest { static void setup() { // Use BASE_URL (without /api suffix) since AgentConfig + HttpApi // already prepend /api to every path. - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -79,22 +79,24 @@ void test_agent_calls_worker_tool() { toolWasCalled.set(false); // reset before each run Agent agent = Agent.builder() - .name("e2e_java_math_agent") - .model(MODEL) - .instructions("You MUST call the add tool with arguments a=7, b=8. Report the result.") - .tools(ToolRegistry.fromInstance(new MathTools())) - .maxTurns(3) - .build(); + .name("e2e_java_math_agent") + .model(MODEL) + .instructions("You MUST call the add tool with arguments a=7, b=8. Report the result.") + .tools(ToolRegistry.fromInstance(new MathTools())) + .maxTurns(3) + .build(); AgentResult result = runtime.run(agent, "What is 7 + 8?"); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Agent did not complete. Status: " + result.getStatus() - + ". Error: " + result.getError()); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent did not complete. Status: " + result.getStatus() + ". Error: " + result.getError()); - assertTrue(toolWasCalled.get(), - "The 'add' tool function body was never called. " - + "COUNTERFACTUAL: if tool registration or the tool dispatch is broken, " - + "the worker is never invoked and this flag stays false."); + assertTrue( + toolWasCalled.get(), + "The 'add' tool function body was never called. " + + "COUNTERFACTUAL: if tool registration or the tool dispatch is broken, " + + "the worker is never invoked and this flag stays false."); } } diff --git a/sdk/java/e2e/Suite2ToolCallingCredentials.java b/sdk/java/e2e/Suite2ToolCallingCredentials.java index 2e6b5fdba..a4467d08d 100644 --- a/sdk/java/e2e/Suite2ToolCallingCredentials.java +++ b/sdk/java/e2e/Suite2ToolCallingCredentials.java @@ -1,15 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.Credentials; -import ai.agentspan.annotations.Tool; -import ai.agentspan.exceptions.AgentspanException; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.net.URI; import java.net.http.HttpClient; @@ -18,7 +10,16 @@ import java.time.Duration; import java.util.*; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.exceptions.AgentspanException; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolContext; +import org.conductoross.conductor.ai.model.ToolDef; +import org.junit.jupiter.api.*; /** * Suite 2 — runtime credential lifecycle, mirrors Python's test_suite2_tool_calling.py. @@ -27,7 +28,7 @@ * runtime injection must verify the same four guarantees: *

      *
    1. No cred in store → tool task TERMINAL-fails (no retries on config bug)
    2. - *
    3. Cred set via API → tool sees the stored value at runtime via {@code Credentials.get()}
    4. + *
    5. Cred set via API → tool sees the stored value at runtime via {@code ctx.getCredential()}
    6. *
    7. Cred updated via API → next run sees the new value (no token snapshotting)
    8. *
    9. Cred deleted → tool task TERMINAL-fails again
    10. *
    @@ -36,7 +37,7 @@ * "env vars not used as fallback" security check from Python's Step 3 is * structurally satisfied by language design. We test it explicitly anyway * (set a JVM-startup env var; verify the SDK doesn't surface it via - * {@code Credentials.get()}).

    + * {@code ctx.getCredential()}).

    * *

    This is the test that would catch URL drift on {@code /api/workers/secrets}, * silent-swallow regressions in {@code WorkerCredentialFetcher}, or any @@ -47,8 +48,8 @@ class Suite2ToolCallingCredentials extends BaseTest { private static final String CRED_A = "E2E_JAVA_CRED_A"; - private static final HttpClient HTTP = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)).build(); + private static final HttpClient HTTP = + HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); private static AgentRuntime runtime; @@ -56,16 +57,14 @@ class Suite2ToolCallingCredentials extends BaseTest { public static class PaidGithubTools { @Tool( - name = "paid_tool_a", - description = "Tool that needs E2E_JAVA_CRED_A. Returns first 3 chars of the credential.", - credentials = {"E2E_JAVA_CRED_A"} - ) - public Map paidToolA(String x) { - String value = Credentials.getOrNull(CRED_A); + name = "paid_tool_a", + description = "Tool that needs E2E_JAVA_CRED_A. Returns first 3 chars of the credential.", + credentials = {"E2E_JAVA_CRED_A"}) + public Map paidToolA(String x, ToolContext ctx) { + String value = ctx.getCredentialOrNull(CRED_A); if (value == null) { - throw new IllegalStateException( - "Credential " + CRED_A + " not in Secrets context. " - + "WorkerManager should have failed the task terminally before reaching here."); + throw new IllegalStateException("Credential " + CRED_A + " not in Secrets context. " + + "WorkerManager should have failed the task terminally before reaching here."); } return Map.of("preview", "paid_a:" + value.substring(0, Math.min(3, value.length()))); } @@ -73,8 +72,7 @@ public Map paidToolA(String x) { @BeforeAll static void setup() { - runtime = new AgentRuntime( - new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -91,8 +89,8 @@ void step1_noCredentialInStore_taskFailsTerminally() { deleteSecret(CRED_A); Agent agent = buildAgent(); - AgentResult result = runtime.run(agent, - "Call paid_tool_a exactly once with the argument 'test' and report what it returns."); + AgentResult result = runtime.run( + agent, "Call paid_tool_a exactly once with the argument 'test' and report what it returns."); assertNotNull(result.getExecutionId(), "result must include an execution id"); @@ -100,10 +98,10 @@ void step1_noCredentialInStore_taskFailsTerminally() { Map wf = getWorkflow(result.getExecutionId()); Set terminal = Set.of("FAILED_WITH_TERMINAL_ERROR", "COMPLETED_WITH_ERRORS"); Map paidTask = findToolTask(wf, "paid_tool_a"); - assertNotNull(paidTask, - "paid_tool_a task not found in workflow — run shape changed?"); + assertNotNull(paidTask, "paid_tool_a task not found in workflow — run shape changed?"); String status = (String) paidTask.get("status"); - assertTrue(terminal.contains(status), + assertTrue( + terminal.contains(status), "Step 1 expected paid_tool_a status in " + terminal + ", got '" + status + "'. Missing credential is a config issue — retries are pointless.\n" + " task=" + paidTask); @@ -122,21 +120,21 @@ void step2_envVarSetButNoStoreValue_envIsNotASilentFallback() { deleteSecret(CRED_A); Agent agent = buildAgent(); - AgentResult result = runtime.run(agent, - "Call paid_tool_a exactly once with 'test' and report what it returns."); + AgentResult result = + runtime.run(agent, "Call paid_tool_a exactly once with 'test' and report what it returns."); Map wf = getWorkflow(result.getExecutionId()); Map paidTask = findToolTask(wf, "paid_tool_a"); Set terminal = Set.of("FAILED_WITH_TERMINAL_ERROR", "COMPLETED_WITH_ERRORS"); - assertTrue(terminal.contains(paidTask.get("status")), - "Java SDK reads secrets only from the server, never from System.getenv. " - + "Got status='" + paidTask.get("status") + "'."); + assertTrue( + terminal.contains(paidTask.get("status")), + "Java SDK reads secrets only from the server, never from System.getenv. " + "Got status='" + + paidTask.get("status") + "'."); // Also: the output should NOT contain anything from System.getenv. // (Tool body never runs when credential missing, but defense in depth.) String output = String.valueOf(paidTask.get("outputData")); - assertFalse(output.contains("paid_a:"), - "tool body should not have run when credential is missing"); + assertFalse(output.contains("paid_a:"), "tool body should not have run when credential is missing"); } // ── Test: cred set via API → tool runs and sees the stored value ───────── @@ -147,19 +145,22 @@ void step3_credentialSet_toolReceivesStoredValue() { putSecret(CRED_A, "secret-aaa-value"); Agent agent = buildAgent(); - AgentResult result = runtime.run(agent, - "Call paid_tool_a exactly once with 'test' and report what it returns."); + AgentResult result = + runtime.run(agent, "Call paid_tool_a exactly once with 'test' and report what it returns."); Map wf = getWorkflow(result.getExecutionId()); Map paidTask = findToolTask(wf, "paid_tool_a"); - assertEquals("COMPLETED", paidTask.get("status"), - "Step 3 expected paid_tool_a COMPLETED, got '" + paidTask.get("status") + "'.\n" - + " task=" + paidTask); + assertEquals( + "COMPLETED", + paidTask.get("status"), + "Step 3 expected paid_tool_a COMPLETED, got '" + paidTask.get("status") + "'.\n" + " task=" + + paidTask); String taskOutput = String.valueOf(paidTask.get("outputData")); - assertTrue(taskOutput.contains("sec"), - "paid_tool_a output should contain 'sec' (first 3 chars of 'secret-aaa-value').\n" - + " outputData=" + taskOutput); + assertTrue( + taskOutput.contains("sec"), + "paid_tool_a output should contain 'sec' (first 3 chars of 'secret-aaa-value').\n" + " outputData=" + + taskOutput); } // ── Test: cred updated → next run reflects new value ───────────────────── @@ -170,15 +171,16 @@ void step4_credentialUpdated_nextRunSeesNewValue() { putSecret(CRED_A, "newval-xxx-updated"); Agent agent = buildAgent(); - AgentResult result = runtime.run(agent, - "Call paid_tool_a exactly once with 'test' and report what it returns."); + AgentResult result = + runtime.run(agent, "Call paid_tool_a exactly once with 'test' and report what it returns."); Map wf = getWorkflow(result.getExecutionId()); Map paidTask = findToolTask(wf, "paid_tool_a"); assertEquals("COMPLETED", paidTask.get("status")); String taskOutput = String.valueOf(paidTask.get("outputData")); - assertTrue(taskOutput.contains("new"), + assertTrue( + taskOutput.contains("new"), "Step 4 expected paid_tool_a output to contain 'new' (first 3 chars of " + "'newval-xxx-updated'). The update didn't propagate.\n" + " outputData=" + taskOutput); @@ -191,9 +193,8 @@ private Agent buildAgent() { return Agent.builder() .name("e2e_java_cred_lifecycle") .model(MODEL) - .instructions( - "You have one tool: paid_tool_a. You MUST call it exactly once " - + "with the argument 'test'. Then report its output verbatim.") + .instructions("You have one tool: paid_tool_a. You MUST call it exactly once " + + "with the argument 'test'. Then report its output verbatim.") .tools(tools) .maxTurns(3) .build(); @@ -224,8 +225,8 @@ private static void putSecret(String name, String value) { .build(); HttpResponse resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString()); if (resp.statusCode() >= 400) { - throw new AgentspanException("PUT /api/secrets/" + name - + " failed: HTTP " + resp.statusCode() + " " + resp.body()); + throw new AgentspanException( + "PUT /api/secrets/" + name + " failed: HTTP " + resp.statusCode() + " " + resp.body()); } } catch (Exception e) { fail("putSecret(" + name + ") failed: " + e); diff --git a/sdk/java/e2e/Suite3CliTools.java b/sdk/java/e2e/Suite3CliTools.java index a6220efca..55c1e6d1c 100644 --- a/sdk/java/e2e/Suite3CliTools.java +++ b/sdk/java/e2e/Suite3CliTools.java @@ -1,31 +1,42 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.execution.CliConfig; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.execution.CliConfig; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.junit.jupiter.api.*; /** - * Suite 13: CLI Tools — structural plan() assertions for {@link CliConfig}. + * Suite 3: CLI Tools — plan-level AND runtime tests for {@link CliConfig}. * - *

    Mirrors Python {@code test_suite3_cli_tools.py}. When an agent is built with - * {@link CliConfig}, the SDK serializes a {@code cliConfig} block on the agentDef - * containing the allowed commands, timeout, allowShell and enabled flags. Server-side - * the agent receives an auto-injected {@code run_command} tool (server contract — not - * asserted client-side). + *

    Mirrors Python {@code test_suite3_cli_tools.py}, TypeScript + * {@code test_suite3_cli_tools.test.ts} and C# {@code Suite11_CliTools}. * - *

    All tests use plan() — no LLM calls. Each assertion has a counterfactual: - * either a contrast assertion in the same test, or a dedicated companion test that - * builds an agent WITHOUT the feature and verifies the field is absent. + *

    Plan-level tests assert the SDK serializes a {@code cliConfig} block + * on the agentDef (allowed commands, timeout, allowShell, enabled) and injects a + * {@code {name}_run_command} worker tool so the LLM can call it. + * + *

    Runtime tests actually run an agent and verify the local + * {@code run_command} worker executes commands and enforces the whitelist — + * the command is executed locally by this SDK + * ({@code org.conductoross.conductor.ai.execution.CliCommandExecutor}), NOT by the server. These + * are the functional checks whose absence previously let a non-functional CLI + * feature ship: the LLM drives the agent, but validation is deterministic + * (literal markers / "not allowed" in the task output), never LLM-judged. + * + *

    Each assertion has a counterfactual: a contrast assertion in the same test, + * or a companion test that builds an agent WITHOUT the feature. */ @Tag("e2e") @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @@ -36,7 +47,7 @@ class Suite3CliTools extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -56,35 +67,47 @@ static void teardown() { @Order(1) void test_cli_config_builder_properties() { CliConfig restrictive = CliConfig.builder() - .allowedCommands(List.of("ls", "mktemp", "gh")) - .timeout(45) - .allowShell(false) - .workingDir("/tmp") - .build(); - - assertTrue(restrictive.isEnabled(), - "CliConfig.enabled defaults to true. COUNTERFACTUAL: if enabled flips, the CLI tool is silently off."); - assertEquals(List.of("ls", "mktemp", "gh"), restrictive.getAllowedCommands(), - "allowedCommands must round-trip. COUNTERFACTUAL: if the list is dropped, the whitelist is empty."); - assertEquals(45, restrictive.getTimeout(), - "timeout must round-trip. COUNTERFACTUAL: if dropped, default 30 returned."); - assertFalse(restrictive.isAllowShell(), - "allowShell=false must round-trip. COUNTERFACTUAL: if dropped, allowShell defaults to false anyway, so assert the contrast below."); - assertEquals("/tmp", restrictive.getWorkingDir(), - "workingDir must round-trip. COUNTERFACTUAL: if dropped, returned null."); + .allowedCommands(List.of("ls", "mktemp", "gh")) + .timeout(45) + .allowShell(false) + .workingDir("/tmp") + .build(); + + assertTrue( + restrictive.isEnabled(), + "CliConfig.enabled defaults to true. COUNTERFACTUAL: if enabled flips, the CLI tool is silently off."); + assertEquals( + List.of("ls", "mktemp", "gh"), + restrictive.getAllowedCommands(), + "allowedCommands must round-trip. COUNTERFACTUAL: if the list is dropped, the whitelist is empty."); + assertEquals( + 45, + restrictive.getTimeout(), + "timeout must round-trip. COUNTERFACTUAL: if dropped, default 30 returned."); + assertFalse( + restrictive.isAllowShell(), + "allowShell=false must round-trip. COUNTERFACTUAL: if dropped, allowShell defaults to false anyway, so assert the contrast below."); + assertEquals( + "/tmp", + restrictive.getWorkingDir(), + "workingDir must round-trip. COUNTERFACTUAL: if dropped, returned null."); // Counterfactual contrast — a permissive config differs on each property CliConfig permissive = CliConfig.builder() - .allowedCommands(List.of("anything")) - .timeout(120) - .allowShell(true) - .build(); - assertNotEquals(restrictive.getAllowedCommands(), permissive.getAllowedCommands(), - "Two builders must produce distinct allowedCommands."); - assertNotEquals(restrictive.getTimeout(), permissive.getTimeout(), - "Two builders must produce distinct timeouts."); - assertNotEquals(restrictive.isAllowShell(), permissive.isAllowShell(), - "Two builders must produce distinct allowShell flags. COUNTERFACTUAL: if allowShell setter is no-op both would be false."); + .allowedCommands(List.of("anything")) + .timeout(120) + .allowShell(true) + .build(); + assertNotEquals( + restrictive.getAllowedCommands(), + permissive.getAllowedCommands(), + "Two builders must produce distinct allowedCommands."); + assertNotEquals( + restrictive.getTimeout(), permissive.getTimeout(), "Two builders must produce distinct timeouts."); + assertNotEquals( + restrictive.isAllowShell(), + permissive.isAllowShell(), + "Two builders must produce distinct allowShell flags. COUNTERFACTUAL: if allowShell setter is no-op both would be false."); } /** @@ -100,42 +123,47 @@ void test_cli_config_builder_properties() { void test_cli_config_serializes_to_agentDef() { List allowed = List.of("ls", "mktemp", "gh"); Agent agent = Agent.builder() - .name("e2e_s13_cli_serialized") - .model(MODEL) - .instructions("Run CLI commands.") - .cliConfig(CliConfig.builder() - .allowedCommands(allowed) - .timeout(60) - .allowShell(false) - .build()) - .build(); - - Map plan = runtime.plan(agent); + .name("e2e_s13_cli_serialized") + .model(MODEL) + .instructions("Run CLI commands.") + .cliConfig(CliConfig.builder() + .allowedCommands(allowed) + .timeout(60) + .allowShell(false) + .build()) + .build(); + + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); Map cliMap = (Map) agentDef.get("cliConfig"); - assertNotNull(cliMap, - "agentDef.cliConfig is null. COUNTERFACTUAL: setting cliConfig on the Agent must serialize " - + "to agentDef.cliConfig. agentDef keys: " + agentDef.keySet()); + assertNotNull( + cliMap, + "agentDef.cliConfig is null. COUNTERFACTUAL: setting cliConfig on the Agent must serialize " + + "to agentDef.cliConfig. agentDef keys: " + agentDef.keySet()); - assertEquals(Boolean.TRUE, cliMap.get("enabled"), - "cliConfig.enabled should be true. Got: " + cliMap.get("enabled")); + assertEquals( + Boolean.TRUE, cliMap.get("enabled"), "cliConfig.enabled should be true. Got: " + cliMap.get("enabled")); List serializedCmds = (List) cliMap.get("allowedCommands"); assertNotNull(serializedCmds, "cliConfig.allowedCommands is null."); - assertEquals(allowed.size(), serializedCmds.size(), - "allowedCommands size mismatch. Expected " + allowed.size() + " but got " + serializedCmds.size()); - assertTrue(serializedCmds.containsAll(allowed), - "allowedCommands must contain all of " + allowed + " but got " + serializedCmds - + ". COUNTERFACTUAL: if a command is dropped, the whitelist diverges."); + assertEquals( + allowed.size(), + serializedCmds.size(), + "allowedCommands size mismatch. Expected " + allowed.size() + " but got " + serializedCmds.size()); + assertTrue( + serializedCmds.containsAll(allowed), + "allowedCommands must contain all of " + allowed + " but got " + serializedCmds + + ". COUNTERFACTUAL: if a command is dropped, the whitelist diverges."); Object timeoutObj = cliMap.get("timeout"); assertNotNull(timeoutObj, "cliConfig.timeout is null"); - assertEquals(60, ((Number) timeoutObj).intValue(), - "cliConfig.timeout should be 60. Got: " + timeoutObj); + assertEquals(60, ((Number) timeoutObj).intValue(), "cliConfig.timeout should be 60. Got: " + timeoutObj); - assertEquals(Boolean.FALSE, cliMap.get("allowShell"), - "cliConfig.allowShell should be false. Got: " + cliMap.get("allowShell")); + assertEquals( + Boolean.FALSE, + cliMap.get("allowShell"), + "cliConfig.allowShell should be false. Got: " + cliMap.get("allowShell")); } /** @@ -148,19 +176,20 @@ void test_cli_config_serializes_to_agentDef() { @Order(3) void test_no_cli_config_means_no_block() { Agent agent = Agent.builder() - .name("e2e_s13_no_cli") - .model(MODEL) - .instructions("No CLI here.") - .build(); + .name("e2e_s13_no_cli") + .model(MODEL) + .instructions("No CLI here.") + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); - assertFalse(agentDef.containsKey("cliConfig"), - "agentDef.cliConfig should be ABSENT for an agent without cliConfig. Got: " - + agentDef.get("cliConfig") - + ". COUNTERFACTUAL: if cliConfig is always emitted, agents that didn't ask " - + "for CLI would still get the run_command tool injected server-side."); + assertFalse( + agentDef.containsKey("cliConfig"), + "agentDef.cliConfig should be ABSENT for an agent without cliConfig. Got: " + + agentDef.get("cliConfig") + + ". COUNTERFACTUAL: if cliConfig is always emitted, agents that didn't ask " + + "for CLI would still get the run_command tool injected server-side."); } /** @@ -180,29 +209,33 @@ void test_no_cli_config_means_no_block() { @SuppressWarnings("unchecked") void test_cli_allow_shell_true_round_trip() { Agent agent = Agent.builder() - .name("e2e_s13_allow_shell") - .model(MODEL) - .instructions("Use shell features.") - .cliConfig(CliConfig.builder() - .allowedCommands(List.of("bash")) - .allowShell(true) - .timeout(15) - .build()) - .build(); - - Map plan = runtime.plan(agent); + .name("e2e_s13_allow_shell") + .model(MODEL) + .instructions("Use shell features.") + .cliConfig(CliConfig.builder() + .allowedCommands(List.of("bash")) + .allowShell(true) + .timeout(15) + .build()) + .build(); + + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); Map cliMap = (Map) agentDef.get("cliConfig"); assertNotNull(cliMap, "agentDef.cliConfig is null."); - assertEquals(Boolean.TRUE, cliMap.get("allowShell"), - "cliConfig.allowShell should be true. Got: " + cliMap.get("allowShell") - + ". COUNTERFACTUAL: paired with the false-case in test_cli_config_serializes_to_agentDef."); + assertEquals( + Boolean.TRUE, + cliMap.get("allowShell"), + "cliConfig.allowShell should be true. Got: " + cliMap.get("allowShell") + + ". COUNTERFACTUAL: paired with the false-case in test_cli_config_serializes_to_agentDef."); Object timeoutObj = cliMap.get("timeout"); - assertEquals(15, ((Number) timeoutObj).intValue(), - "cliConfig.timeout should be 15. Got: " + timeoutObj - + ". COUNTERFACTUAL: different timeout from test_cli_config_serializes_to_agentDef so a stuck value would be caught."); + assertEquals( + 15, + ((Number) timeoutObj).intValue(), + "cliConfig.timeout should be 15. Got: " + timeoutObj + + ". COUNTERFACTUAL: different timeout from test_cli_config_serializes_to_agentDef so a stuck value would be caught."); } /** @@ -216,26 +249,26 @@ void test_cli_allow_shell_true_round_trip() { @SuppressWarnings("unchecked") void test_two_agents_have_distinct_cli_configs() { Agent agentA = Agent.builder() - .name("e2e_s13_agent_a") - .model(MODEL) - .instructions("A.") - .cliConfig(CliConfig.builder() - .allowedCommands(List.of("ls")) - .timeout(10) - .build()) - .build(); + .name("e2e_s13_agent_a") + .model(MODEL) + .instructions("A.") + .cliConfig(CliConfig.builder() + .allowedCommands(List.of("ls")) + .timeout(10) + .build()) + .build(); Agent agentB = Agent.builder() - .name("e2e_s13_agent_b") - .model(MODEL) - .instructions("B.") - .cliConfig(CliConfig.builder() - .allowedCommands(List.of("gh", "git")) - .timeout(90) - .build()) - .build(); - - Map planA = runtime.plan(agentA); - Map planB = runtime.plan(agentB); + .name("e2e_s13_agent_b") + .model(MODEL) + .instructions("B.") + .cliConfig(CliConfig.builder() + .allowedCommands(List.of("gh", "git")) + .timeout(90) + .build()) + .build(); + + CompileResponse planA = runtime.plan(agentA); + CompileResponse planB = runtime.plan(agentB); Map cliA = (Map) getAgentDef(planA).get("cliConfig"); Map cliB = (Map) getAgentDef(planB).get("cliConfig"); @@ -246,18 +279,22 @@ void test_two_agents_have_distinct_cli_configs() { List cmdsA = (List) cliA.get("allowedCommands"); List cmdsB = (List) cliB.get("allowedCommands"); - assertTrue(cmdsA.contains("ls") && !cmdsA.contains("gh"), - "agentA should have 'ls' only. Got: " + cmdsA - + ". COUNTERFACTUAL: if state leaks, agentA would also have 'gh' from agentB."); - assertTrue(cmdsB.contains("gh") && !cmdsB.contains("ls"), - "agentB should have 'gh' but not 'ls'. Got: " + cmdsB - + ". COUNTERFACTUAL: if state leaks, agentB would have 'ls' from agentA."); + assertTrue( + cmdsA.contains("ls") && !cmdsA.contains("gh"), + "agentA should have 'ls' only. Got: " + cmdsA + + ". COUNTERFACTUAL: if state leaks, agentA would also have 'gh' from agentB."); + assertTrue( + cmdsB.contains("gh") && !cmdsB.contains("ls"), + "agentB should have 'gh' but not 'ls'. Got: " + cmdsB + + ". COUNTERFACTUAL: if state leaks, agentB would have 'ls' from agentA."); int timeoutA = ((Number) cliA.get("timeout")).intValue(); int timeoutB = ((Number) cliB.get("timeout")).intValue(); - assertNotEquals(timeoutA, timeoutB, - "Timeouts must differ between agents (10 vs 90). Got: " + timeoutA + " vs " + timeoutB - + ". COUNTERFACTUAL: if shared, the two agents collapse onto one config."); + assertNotEquals( + timeoutA, + timeoutB, + "Timeouts must differ between agents (10 vs 90). Got: " + timeoutA + " vs " + timeoutB + + ". COUNTERFACTUAL: if shared, the two agents collapse onto one config."); } /** @@ -273,26 +310,172 @@ void test_two_agents_have_distinct_cli_configs() { @SuppressWarnings("unchecked") void test_cli_does_not_inject_user_tool() { Agent agent = Agent.builder() - .name("e2e_s13_no_user_tool_injection") - .model(MODEL) - .instructions("Use CLI.") - .cliConfig(CliConfig.builder() - .allowedCommands(List.of("ls", "mktemp")) - .build()) - .build(); - - Map plan = runtime.plan(agent); + .name("e2e_s13_no_user_tool_injection") + .model(MODEL) + .instructions("Use CLI.") + .cliConfig(CliConfig.builder() + .allowedCommands(List.of("ls", "mktemp")) + .build()) + .build(); + + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); List> tools = (List>) agentDef.get("tools"); - List toolNames = tools == null ? List.of() - : tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList()); - - assertFalse(toolNames.contains("ls"), - "agentDef.tools must NOT contain a tool literally named 'ls' — that would be a leak from " - + "cliConfig.allowedCommands into the user tools list. Tools: " + toolNames - + ". COUNTERFACTUAL: this fails if the serializer mistakenly registers each allowed command as a tool."); - assertFalse(toolNames.contains("mktemp"), - "agentDef.tools must NOT contain a tool literally named 'mktemp'. Tools: " + toolNames); + List toolNames = tools == null + ? List.of() + : tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList()); + + assertFalse( + toolNames.contains("ls"), + "agentDef.tools must NOT contain a tool literally named 'ls' — that would be a leak from " + + "cliConfig.allowedCommands into the user tools list. Tools: " + toolNames + + ". COUNTERFACTUAL: this fails if the serializer mistakenly registers each allowed command as a tool."); + assertFalse( + toolNames.contains("mktemp"), + "agentDef.tools must NOT contain a tool literally named 'mktemp'. Tools: " + toolNames); + } + + // ── Runtime tests ───────────────────────────────────────────────────────── + + /** Find workflow tasks belonging to the run_command worker. */ + @SuppressWarnings("unchecked") + private List> findRunCommandTasks(String executionId) { + Map workflow = getWorkflow(executionId); + List> allTasks = (List>) workflow.get("tasks"); + if (allTasks == null) return List.of(); + return allTasks.stream() + .filter(t -> { + String ref = (String) t.getOrDefault("referenceTaskName", ""); + String defName = (String) t.getOrDefault("taskDefName", ""); + String taskType = (String) t.getOrDefault("taskType", ""); + return ref.contains("run_command") + || defName.contains("run_command") + || taskType.contains("run_command"); + }) + .collect(Collectors.toList()); + } + + private String taskOutputStr(Map task) { + return String.valueOf(task.getOrDefault("outputData", "")); + } + + /** + * Runtime: the local run_command worker actually executes a command and the + * output flows back into the run_command task. This is the functional check + * that proves the CLI feature is wired end-to-end (tool injected → SIMPLE task + * created → SDK worker polls → command executed locally → output captured). + * + *

    Runs {@code echo cli_marker_3066}; the literal marker must appear in a + * run_command task's output. The marker is unique so it can only come from the + * command actually running — not from the prompt being echoed by the LLM. + * + * COUNTERFACTUAL: if no run_command worker is registered (the previous state of + * the Java SDK), no run_command task produces output containing the marker. + */ + @Test + @Order(7) + @Timeout(value = 300, unit = TimeUnit.SECONDS) + void test_local_cli_execution() { + Agent agent = Agent.builder() + .name("e2e_s3_cli_exec") + .model(MODEL) + .instructions("You run shell commands with the run_command tool. " + + "When asked to run a command, you MUST call run_command with the exact " + + "command given. Never fabricate output — always call the tool.") + .cliConfig(CliConfig.builder() + .allowedCommands(List.of("echo")) + .timeout(30) + .build()) + .maxTurns(5) + .build(); + + AgentResult result = runtime.run(agent, "Run this exact command using run_command: echo cli_marker_3066"); + + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Agent with CLI execution should complete. Status: " + result.getStatus() + ". Error: " + + result.getError()); + + String executionId = result.getExecutionId(); + assertNotNull(executionId, "executionId is null"); + + List> tasks = findRunCommandTasks(executionId); + assertFalse( + tasks.isEmpty(), + "No run_command task found in workflow. COUNTERFACTUAL: if the run_command " + + "tool is not injected or no worker is registered to execute it, no such task appears."); + + boolean foundMarker = tasks.stream().anyMatch(t -> taskOutputStr(t).contains("cli_marker_3066")); + assertTrue( + foundMarker, + "Expected 'cli_marker_3066' in a run_command task output — proves the local " + + "worker actually executed `echo`. Outputs: " + + tasks.stream() + .map(t -> taskOutputStr(t) + .substring( + 0, + Math.min(200, taskOutputStr(t).length()))) + .collect(Collectors.toList()) + + ". COUNTERFACTUAL: a non-functional executor produces no marker."); + } + + /** + * Runtime: the local worker enforces the whitelist. An agent allowed only + * {@code echo} that is asked to run {@code ls} must have its run_command task + * report the command as not allowed — proving validation runs in the worker, + * not just in the prompt. + * + * COUNTERFACTUAL: if the worker skipped validation, {@code ls} would execute + * (directory listing in stdout) and "not allowed" would be absent. + */ + @Test + @Order(8) + @Timeout(value = 300, unit = TimeUnit.SECONDS) + void test_cli_whitelist_blocks_disallowed_command() { + Agent agent = Agent.builder() + .name("e2e_s3_cli_whitelist") + .model(MODEL) + .instructions("You run shell commands with the run_command tool. " + + "When asked to run a command, call run_command with that exact command, " + + "even if you suspect it may be rejected. Report what the tool returns.") + .cliConfig(CliConfig.builder() + .allowedCommands(List.of("echo")) // ls is NOT allowed + .timeout(30) + .build()) + .maxTurns(5) + .build(); + + AgentResult result = runtime.run(agent, "Use run_command to run exactly: ls -la /"); + + // Terminal status either way — the worker returns an error result, it does + // not fail the task; the LLM may then complete gracefully. + assertTrue( + result.getStatus() == AgentStatus.COMPLETED + || result.getStatus() == AgentStatus.FAILED + || result.getStatus() == AgentStatus.TERMINATED, + "Expected a terminal status. Got: " + result.getStatus()); + + String executionId = result.getExecutionId(); + assertNotNull(executionId, "executionId is null"); + + List> tasks = findRunCommandTasks(executionId); + assertFalse( + tasks.isEmpty(), "No run_command task found — the LLM should have attempted the disallowed command."); + + boolean blocked = + tasks.stream().anyMatch(t -> taskOutputStr(t).toLowerCase().contains("is not allowed")); + assertTrue( + blocked, + "Expected a run_command task to report 'is not allowed' for `ls`. Outputs: " + + tasks.stream() + .map(t -> taskOutputStr(t) + .substring( + 0, + Math.min(200, taskOutputStr(t).length()))) + .collect(Collectors.toList()) + + ". COUNTERFACTUAL: if the worker skipped whitelist validation, `ls` would run " + + "and there would be no 'not allowed' message."); } } diff --git a/sdk/java/e2e/Suite4McpTools.java b/sdk/java/e2e/Suite4McpTools.java index c15fa058e..3d7cfb3cd 100644 --- a/sdk/java/e2e/Suite4McpTools.java +++ b/sdk/java/e2e/Suite4McpTools.java @@ -1,19 +1,20 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.model.ToolDef; -import ai.agentspan.tools.McpTool; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.tools.McpTool; +import org.junit.jupiter.api.*; /** * Suite 14: MCP Tools — structural plan() assertions for {@link McpTool}. @@ -32,7 +33,7 @@ class Suite4McpTools extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -47,13 +48,13 @@ private Map findToolByName(Map agentDef, String List> tools = (List>) agentDef.get("tools"); assertNotNull(tools, "agentDef has no 'tools' key"); return tools.stream() - .filter(t -> name.equals(t.get("name"))) - .findFirst() - .orElseGet(() -> { - fail("Tool '" + name + "' not found. Available: " - + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); - return null; - }); + .filter(t -> name.equals(t.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("Tool '" + name + "' not found. Available: " + + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); + return null; + }); } // ── Tests ───────────────────────────────────────────────────────────────── @@ -69,35 +70,37 @@ private Map findToolByName(Map agentDef, String @Order(1) void test_mcp_tool_def_basic_properties() { ToolDef mcp = McpTool.builder() - .name("e2e_s14_basic") - .description("Basic MCP tool") - .serverUrl("http://localhost:9999/mcp") - .toolName("math_add") - .build(); - - assertEquals("e2e_s14_basic", mcp.getName(), - "MCP tool name must round-trip."); - assertEquals("Basic MCP tool", mcp.getDescription(), - "MCP tool description must round-trip."); - assertEquals("mcp", mcp.getToolType(), - "MCP tool toolType must be 'mcp'. Got: " + mcp.getToolType() - + ". COUNTERFACTUAL: paired with the contrast assertion below."); - assertNull(mcp.getFunc(), - "MCP tool must NOT have a local func — the server handles execution. Got: " + mcp.getFunc()); + .name("e2e_s14_basic") + .description("Basic MCP tool") + .serverUrl("http://localhost:9999/mcp") + .toolName("math_add") + .build(); + + assertEquals("e2e_s14_basic", mcp.getName(), "MCP tool name must round-trip."); + assertEquals("Basic MCP tool", mcp.getDescription(), "MCP tool description must round-trip."); + assertEquals( + "mcp", + mcp.getToolType(), + "MCP tool toolType must be 'mcp'. Got: " + mcp.getToolType() + + ". COUNTERFACTUAL: paired with the contrast assertion below."); + assertNull( + mcp.getFunc(), + "MCP tool must NOT have a local func — the server handles execution. Got: " + mcp.getFunc()); // Counterfactual contrast — a worker ToolDef must NOT be classified as mcp. ToolDef worker = ToolDef.builder() - .name("e2e_s14_contrast_worker") - .description("A worker tool") - .inputSchema(Map.of("type", "object", "properties", Map.of())) - .toolType("worker") - .func(input -> input) - .build(); - assertEquals("worker", worker.getToolType(), - "Worker tool must remain toolType='worker'."); - assertNotEquals(mcp.getToolType(), worker.getToolType(), - "MCP and worker tools must have distinct toolTypes. COUNTERFACTUAL: if McpTool.builder() doesn't " - + "override the default 'worker', these would collide."); + .name("e2e_s14_contrast_worker") + .description("A worker tool") + .inputSchema(Map.of("type", "object", "properties", Map.of())) + .toolType("worker") + .func(input -> input) + .build(); + assertEquals("worker", worker.getToolType(), "Worker tool must remain toolType='worker'."); + assertNotEquals( + mcp.getToolType(), + worker.getToolType(), + "MCP and worker tools must have distinct toolTypes. COUNTERFACTUAL: if McpTool.builder() doesn't " + + "override the default 'worker', these would collide."); } /** @@ -110,29 +113,28 @@ void test_mcp_tool_def_basic_properties() { @Order(2) void test_mcp_tool_credentials_round_trip() { ToolDef authed = McpTool.builder() - .name("e2e_s14_authed") - .description("MCP tool with auth") - .serverUrl("http://localhost:9999/mcp") - .header("Authorization", "Bearer ${MCP_AUTH_KEY}") - .credentials("MCP_AUTH_KEY") - .build(); + .name("e2e_s14_authed") + .description("MCP tool with auth") + .serverUrl("http://localhost:9999/mcp") + .header("Authorization", "Bearer ${MCP_AUTH_KEY}") + .credentials("MCP_AUTH_KEY") + .build(); List creds = authed.getCredentials(); assertNotNull(creds, "credentials list is null"); - assertEquals(1, creds.size(), - "Expected exactly 1 credential. Got: " + creds); - assertEquals("MCP_AUTH_KEY", creds.get(0), - "Credential should be 'MCP_AUTH_KEY'. Got: " + creds.get(0)); + assertEquals(1, creds.size(), "Expected exactly 1 credential. Got: " + creds); + assertEquals("MCP_AUTH_KEY", creds.get(0), "Credential should be 'MCP_AUTH_KEY'. Got: " + creds.get(0)); // Counterfactual: a no-credentials tool must produce an empty list. ToolDef unauthed = McpTool.builder() - .name("e2e_s14_unauthed") - .description("MCP tool without auth") - .serverUrl("http://localhost:9999/mcp") - .build(); - assertTrue(unauthed.getCredentials() == null || unauthed.getCredentials().isEmpty(), - "Unauthenticated MCP tool must have no credentials. Got: " + unauthed.getCredentials() - + ". COUNTERFACTUAL: if credentials() always added 'MCP_AUTH_KEY', this would fail."); + .name("e2e_s14_unauthed") + .description("MCP tool without auth") + .serverUrl("http://localhost:9999/mcp") + .build(); + assertTrue( + unauthed.getCredentials() == null || unauthed.getCredentials().isEmpty(), + "Unauthenticated MCP tool must have no credentials. Got: " + unauthed.getCredentials() + + ". COUNTERFACTUAL: if credentials() always added 'MCP_AUTH_KEY', this would fail."); } /** @@ -146,38 +148,43 @@ void test_mcp_tool_credentials_round_trip() { @SuppressWarnings("unchecked") void test_mcp_tool_serializes_to_plan() { ToolDef mcp = McpTool.builder() - .name("e2e_s14_plan") - .description("MCP tool in plan") - .serverUrl("http://localhost:9999/mcp") - .header("X-Test", "yes") - .build(); + .name("e2e_s14_plan") + .description("MCP tool in plan") + .serverUrl("http://localhost:9999/mcp") + .header("X-Test", "yes") + .build(); Agent agent = Agent.builder() - .name("e2e_s14_agent_with_mcp") - .model(MODEL) - .instructions("Use MCP.") - .tools(List.of(mcp)) - .build(); + .name("e2e_s14_agent_with_mcp") + .model(MODEL) + .instructions("Use MCP.") + .tools(List.of(mcp)) + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); Map tool = findToolByName(agentDef, "e2e_s14_plan"); - assertEquals("mcp", tool.get("toolType"), - "Plan toolType should be 'mcp'. Got: " + tool.get("toolType") - + ". COUNTERFACTUAL: if serializer drops it, server won't treat it as MCP."); + assertEquals( + "mcp", + tool.get("toolType"), + "Plan toolType should be 'mcp'. Got: " + tool.get("toolType") + + ". COUNTERFACTUAL: if serializer drops it, server won't treat it as MCP."); Map config = (Map) tool.get("config"); - assertNotNull(config, - "MCP tool plan must carry config (serverUrl, headers). Got null config."); - assertEquals("http://localhost:9999/mcp", config.get("serverUrl"), - "config.serverUrl should round-trip. Got: " + config.get("serverUrl")); + assertNotNull(config, "MCP tool plan must carry config (serverUrl, headers). Got null config."); + assertEquals( + "http://localhost:9999/mcp", + config.get("serverUrl"), + "config.serverUrl should round-trip. Got: " + config.get("serverUrl")); Map headers = (Map) config.get("headers"); assertNotNull(headers, "config.headers should be present."); - assertEquals("yes", headers.get("X-Test"), - "Header 'X-Test' should be 'yes'. Got: " + headers.get("X-Test") - + ". COUNTERFACTUAL: if headers are lost, MCP auth headers would not reach the server."); + assertEquals( + "yes", + headers.get("X-Test"), + "Header 'X-Test' should be 'yes'. Got: " + headers.get("X-Test") + + ". COUNTERFACTUAL: if headers are lost, MCP auth headers would not reach the server."); } /** @@ -191,31 +198,34 @@ void test_mcp_tool_serializes_to_plan() { @SuppressWarnings("unchecked") void test_no_mcp_tool_means_no_mcp_in_plan() { ToolDef worker = ToolDef.builder() - .name("e2e_s14_just_worker") - .description("Plain worker.") - .inputSchema(Map.of("type", "object", "properties", Map.of())) - .toolType("worker") - .func(input -> input) - .build(); + .name("e2e_s14_just_worker") + .description("Plain worker.") + .inputSchema(Map.of("type", "object", "properties", Map.of())) + .toolType("worker") + .func(input -> input) + .build(); Agent agent = Agent.builder() - .name("e2e_s14_no_mcp_agent") - .model(MODEL) - .instructions("No MCP here.") - .tools(List.of(worker)) - .build(); + .name("e2e_s14_no_mcp_agent") + .model(MODEL) + .instructions("No MCP here.") + .tools(List.of(worker)) + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); List> tools = (List>) agentDef.get("tools"); assertNotNull(tools, "agentDef.tools is null"); boolean anyMcp = tools.stream().anyMatch(t -> "mcp".equals(t.get("toolType"))); - assertFalse(anyMcp, - "Plan must contain NO toolType='mcp' entries when no MCP tool was added. Got tools: " - + tools.stream().map(t -> t.get("name") + "[" + t.get("toolType") + "]").collect(Collectors.toList()) - + ". COUNTERFACTUAL: if every tool were serialized as mcp, the server would broker non-MCP tools incorrectly."); + assertFalse( + anyMcp, + "Plan must contain NO toolType='mcp' entries when no MCP tool was added. Got tools: " + + tools.stream() + .map(t -> t.get("name") + "[" + t.get("toolType") + "]") + .collect(Collectors.toList()) + + ". COUNTERFACTUAL: if every tool were serialized as mcp, the server would broker non-MCP tools incorrectly."); } /** @@ -229,21 +239,21 @@ void test_no_mcp_tool_means_no_mcp_in_plan() { @SuppressWarnings("unchecked") void test_mcp_credentials_nested_in_plan_config() { ToolDef mcp = McpTool.builder() - .name("e2e_s14_creds_plan") - .description("MCP with credentials") - .serverUrl("http://localhost:9999/mcp") - .header("Authorization", "Bearer ${MCP_AUTH_KEY}") - .credentials("MCP_AUTH_KEY") - .build(); + .name("e2e_s14_creds_plan") + .description("MCP with credentials") + .serverUrl("http://localhost:9999/mcp") + .header("Authorization", "Bearer ${MCP_AUTH_KEY}") + .credentials("MCP_AUTH_KEY") + .build(); Agent agent = Agent.builder() - .name("e2e_s14_creds_agent") - .model(MODEL) - .instructions("Use authed MCP.") - .tools(List.of(mcp)) - .build(); + .name("e2e_s14_creds_agent") + .model(MODEL) + .instructions("Use authed MCP.") + .tools(List.of(mcp)) + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); Map tool = findToolByName(agentDef, "e2e_s14_creds_plan"); @@ -251,33 +261,33 @@ void test_mcp_credentials_nested_in_plan_config() { assertNotNull(config, "config null on credentialled MCP tool"); List creds = (List) config.get("credentials"); - assertNotNull(creds, - "config.credentials should be present. config: " + config - + ". COUNTERFACTUAL: if credentials aren't nested under config, the server token won't declare them."); - assertEquals(1, creds.size(), - "Should have exactly one credential. Got: " + creds); - assertEquals("MCP_AUTH_KEY", creds.get(0), - "Credential name should be 'MCP_AUTH_KEY'. Got: " + creds.get(0)); + assertNotNull( + creds, + "config.credentials should be present. config: " + config + + ". COUNTERFACTUAL: if credentials aren't nested under config, the server token won't declare them."); + assertEquals(1, creds.size(), "Should have exactly one credential. Got: " + creds); + assertEquals("MCP_AUTH_KEY", creds.get(0), "Credential name should be 'MCP_AUTH_KEY'. Got: " + creds.get(0)); // Contrast — uncredentialled tool should not have credentials in config ToolDef noCred = McpTool.builder() - .name("e2e_s14_no_creds_plan") - .description("MCP without credentials") - .serverUrl("http://localhost:9999/mcp") - .build(); + .name("e2e_s14_no_creds_plan") + .description("MCP without credentials") + .serverUrl("http://localhost:9999/mcp") + .build(); Agent agent2 = Agent.builder() - .name("e2e_s14_no_creds_agent") - .model(MODEL) - .instructions("Plain MCP.") - .tools(List.of(noCred)) - .build(); + .name("e2e_s14_no_creds_agent") + .model(MODEL) + .instructions("Plain MCP.") + .tools(List.of(noCred)) + .build(); Map agentDef2 = getAgentDef(runtime.plan(agent2)); Map tool2 = findToolByName(agentDef2, "e2e_s14_no_creds_plan"); Map config2 = (Map) tool2.get("config"); if (config2 != null) { - assertNull(config2.get("credentials"), - "Unauthenticated MCP tool must have NO credentials in config. Got: " + config2.get("credentials") - + ". COUNTERFACTUAL: if credentials() always emits a list, the contrast fails."); + assertNull( + config2.get("credentials"), + "Unauthenticated MCP tool must have NO credentials in config. Got: " + config2.get("credentials") + + ". COUNTERFACTUAL: if credentials() always emits a list, the contrast fails."); } } @@ -293,36 +303,40 @@ void test_mcp_credentials_nested_in_plan_config() { @SuppressWarnings("unchecked") void test_mcp_and_worker_tools_coexist() { ToolDef mcp = McpTool.builder() - .name("e2e_s14_compose_mcp") - .description("MCP") - .serverUrl("http://localhost:9999/mcp") - .build(); + .name("e2e_s14_compose_mcp") + .description("MCP") + .serverUrl("http://localhost:9999/mcp") + .build(); ToolDef worker = ToolDef.builder() - .name("e2e_s14_compose_worker") - .description("worker") - .inputSchema(Map.of("type", "object", "properties", Map.of())) - .toolType("worker") - .func(input -> input) - .build(); + .name("e2e_s14_compose_worker") + .description("worker") + .inputSchema(Map.of("type", "object", "properties", Map.of())) + .toolType("worker") + .func(input -> input) + .build(); Agent agent = Agent.builder() - .name("e2e_s14_compose_agent") - .model(MODEL) - .instructions("Mixed tools.") - .tools(List.of(mcp, worker)) - .build(); + .name("e2e_s14_compose_agent") + .model(MODEL) + .instructions("Mixed tools.") + .tools(List.of(mcp, worker)) + .build(); Map agentDef = getAgentDef(runtime.plan(agent)); List> tools = (List>) agentDef.get("tools"); assertNotNull(tools, "tools list is null"); Map typeByName = tools.stream() - .collect(Collectors.toMap(t -> (String) t.get("name"), t -> (String) t.get("toolType"), (a, b) -> a)); - - assertEquals("mcp", typeByName.get("e2e_s14_compose_mcp"), - "MCP tool must serialize as toolType='mcp'. Got: " + typeByName.get("e2e_s14_compose_mcp")); - assertEquals("worker", typeByName.get("e2e_s14_compose_worker"), - "Worker tool must serialize as toolType='worker'. Got: " + typeByName.get("e2e_s14_compose_worker") - + ". COUNTERFACTUAL: if MCP serialization clobbered every tool, this would be 'mcp'."); + .collect(Collectors.toMap(t -> (String) t.get("name"), t -> (String) t.get("toolType"), (a, b) -> a)); + + assertEquals( + "mcp", + typeByName.get("e2e_s14_compose_mcp"), + "MCP tool must serialize as toolType='mcp'. Got: " + typeByName.get("e2e_s14_compose_mcp")); + assertEquals( + "worker", + typeByName.get("e2e_s14_compose_worker"), + "Worker tool must serialize as toolType='worker'. Got: " + typeByName.get("e2e_s14_compose_worker") + + ". COUNTERFACTUAL: if MCP serialization clobbered every tool, this would be 'mcp'."); } } diff --git a/sdk/java/e2e/Suite5HttpTools.java b/sdk/java/e2e/Suite5HttpTools.java index f9a288439..6e14d6f27 100644 --- a/sdk/java/e2e/Suite5HttpTools.java +++ b/sdk/java/e2e/Suite5HttpTools.java @@ -1,19 +1,19 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.model.ToolDef; -import ai.agentspan.tools.HttpTool; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.tools.HttpTool; +import org.junit.jupiter.api.*; /** * Suite 15: HTTP Tools — structural plan() assertions for {@link HttpTool}. @@ -32,7 +32,7 @@ class Suite5HttpTools extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -47,13 +47,13 @@ private Map findToolByName(Map agentDef, String List> tools = (List>) agentDef.get("tools"); assertNotNull(tools, "agentDef has no 'tools' key"); return tools.stream() - .filter(t -> name.equals(t.get("name"))) - .findFirst() - .orElseGet(() -> { - fail("Tool '" + name + "' not found. Available: " - + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); - return null; - }); + .filter(t -> name.equals(t.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("Tool '" + name + "' not found. Available: " + + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); + return null; + }); } // ── Tests ───────────────────────────────────────────────────────────────── @@ -68,30 +68,34 @@ private Map findToolByName(Map agentDef, String @Order(1) void test_http_tool_def_basic_properties() { ToolDef http = HttpTool.builder() - .name("e2e_s15_basic") - .description("Basic HTTP tool") - .url("http://localhost:9999/api/math/add") - .method("GET") - .build(); - - assertEquals("e2e_s15_basic", http.getName(), - "HTTP tool name must round-trip."); - assertEquals("http", http.getToolType(), - "HTTP tool toolType must be 'http'. Got: " + http.getToolType() - + ". COUNTERFACTUAL: paired with the contrast below."); - assertNull(http.getFunc(), - "HTTP tool must NOT carry a local func — the server makes the request. Got: " + http.getFunc()); + .name("e2e_s15_basic") + .description("Basic HTTP tool") + .url("http://localhost:9999/api/math/add") + .method("GET") + .build(); + + assertEquals("e2e_s15_basic", http.getName(), "HTTP tool name must round-trip."); + assertEquals( + "http", + http.getToolType(), + "HTTP tool toolType must be 'http'. Got: " + http.getToolType() + + ". COUNTERFACTUAL: paired with the contrast below."); + assertNull( + http.getFunc(), + "HTTP tool must NOT carry a local func — the server makes the request. Got: " + http.getFunc()); ToolDef worker = ToolDef.builder() - .name("e2e_s15_contrast_worker") - .description("worker") - .inputSchema(Map.of("type", "object", "properties", Map.of())) - .toolType("worker") - .func(input -> input) - .build(); - assertNotEquals(http.getToolType(), worker.getToolType(), - "HTTP and worker tools must have distinct toolTypes. COUNTERFACTUAL: if HttpTool.build() " - + "doesn't override the default 'worker' toolType, both would be 'worker'."); + .name("e2e_s15_contrast_worker") + .description("worker") + .inputSchema(Map.of("type", "object", "properties", Map.of())) + .toolType("worker") + .func(input -> input) + .build(); + assertNotEquals( + http.getToolType(), + worker.getToolType(), + "HTTP and worker tools must have distinct toolTypes. COUNTERFACTUAL: if HttpTool.build() " + + "doesn't override the default 'worker' toolType, both would be 'worker'."); } /** @@ -103,19 +107,21 @@ void test_http_tool_def_basic_properties() { @Test @Order(2) void test_http_tool_requires_url() { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, - () -> HttpTool.builder().name("e2e_s15_no_url").build(), - "HttpTool.builder() with no URL should throw IllegalArgumentException. " - + "COUNTERFACTUAL: if the validator is missing, build() would succeed and " - + "the server would receive a tool with a null URL."); - assertTrue(ex.getMessage().toLowerCase().contains("url"), - "Error message should mention 'url'. Got: " + ex.getMessage()); + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> HttpTool.builder().name("e2e_s15_no_url").build(), + "HttpTool.builder() with no URL should throw IllegalArgumentException. " + + "COUNTERFACTUAL: if the validator is missing, build() would succeed and " + + "the server would receive a tool with a null URL."); + assertTrue( + ex.getMessage().toLowerCase().contains("url"), + "Error message should mention 'url'. Got: " + ex.getMessage()); // Counterfactual contrast: providing a URL allows build to succeed. ToolDef ok = HttpTool.builder() - .name("e2e_s15_with_url") - .url("http://example.com") - .build(); + .name("e2e_s15_with_url") + .url("http://example.com") + .build(); assertNotNull(ok, "Build with URL should succeed."); } @@ -130,39 +136,44 @@ void test_http_tool_requires_url() { @SuppressWarnings("unchecked") void test_http_tool_serializes_to_plan() { ToolDef http = HttpTool.builder() - .name("e2e_s15_plan") - .description("API call") - .url("https://api.example.com/v1/widgets") - .method("POST") - .header("Content-Type", "application/json") - .build(); + .name("e2e_s15_plan") + .description("API call") + .url("https://api.example.com/v1/widgets") + .method("POST") + .header("Content-Type", "application/json") + .build(); Agent agent = Agent.builder() - .name("e2e_s15_agent_with_http") - .model(MODEL) - .instructions("Use HTTP.") - .tools(List.of(http)) - .build(); + .name("e2e_s15_agent_with_http") + .model(MODEL) + .instructions("Use HTTP.") + .tools(List.of(http)) + .build(); Map agentDef = getAgentDef(runtime.plan(agent)); Map tool = findToolByName(agentDef, "e2e_s15_plan"); - assertEquals("http", tool.get("toolType"), - "Plan toolType should be 'http'. Got: " + tool.get("toolType")); + assertEquals("http", tool.get("toolType"), "Plan toolType should be 'http'. Got: " + tool.get("toolType")); Map config = (Map) tool.get("config"); assertNotNull(config, "HTTP tool plan must include config (url, method, headers)."); - assertEquals("https://api.example.com/v1/widgets", config.get("url"), - "config.url should round-trip. Got: " + config.get("url") - + ". COUNTERFACTUAL: if the URL is dropped, the server has nothing to call."); - assertEquals("POST", config.get("method"), - "config.method should be 'POST'. Got: " + config.get("method") - + ". COUNTERFACTUAL: paired with the GET-method check below."); + assertEquals( + "https://api.example.com/v1/widgets", + config.get("url"), + "config.url should round-trip. Got: " + config.get("url") + + ". COUNTERFACTUAL: if the URL is dropped, the server has nothing to call."); + assertEquals( + "POST", + config.get("method"), + "config.method should be 'POST'. Got: " + config.get("method") + + ". COUNTERFACTUAL: paired with the GET-method check below."); Map headers = (Map) config.get("headers"); assertNotNull(headers, "config.headers must be present."); - assertEquals("application/json", headers.get("Content-Type"), - "Content-Type header should round-trip. Got: " + headers.get("Content-Type")); + assertEquals( + "application/json", + headers.get("Content-Type"), + "Content-Type header should round-trip. Got: " + headers.get("Content-Type")); } /** @@ -175,22 +186,22 @@ void test_http_tool_serializes_to_plan() { @SuppressWarnings("unchecked") void test_http_method_distinct_in_plan() { ToolDef getTool = HttpTool.builder() - .name("e2e_s15_get") - .url("https://api.example.com/get") - .method("GET") - .build(); + .name("e2e_s15_get") + .url("https://api.example.com/get") + .method("GET") + .build(); ToolDef postTool = HttpTool.builder() - .name("e2e_s15_post") - .url("https://api.example.com/post") - .method("POST") - .build(); + .name("e2e_s15_post") + .url("https://api.example.com/post") + .method("POST") + .build(); Agent agent = Agent.builder() - .name("e2e_s15_method_agent") - .model(MODEL) - .instructions("Two HTTPs.") - .tools(List.of(getTool, postTool)) - .build(); + .name("e2e_s15_method_agent") + .model(MODEL) + .instructions("Two HTTPs.") + .tools(List.of(getTool, postTool)) + .build(); Map agentDef = getAgentDef(runtime.plan(agent)); Map g = findToolByName(agentDef, "e2e_s15_get"); @@ -201,9 +212,11 @@ void test_http_method_distinct_in_plan() { assertEquals("GET", getMethod, "GET tool method should be 'GET'. Got: " + getMethod); assertEquals("POST", postMethod, "POST tool method should be 'POST'. Got: " + postMethod); - assertNotEquals(getMethod, postMethod, - "GET and POST tools must have distinct serialized methods. " - + "COUNTERFACTUAL: if method() always set the same value, this would fail."); + assertNotEquals( + getMethod, + postMethod, + "GET and POST tools must have distinct serialized methods. " + + "COUNTERFACTUAL: if method() always set the same value, this would fail."); } /** @@ -216,52 +229,52 @@ void test_http_method_distinct_in_plan() { @SuppressWarnings("unchecked") void test_http_credentials_nested_in_plan_config() { ToolDef http = HttpTool.builder() - .name("e2e_s15_authed") - .url("https://api.example.com/secure") - .method("GET") - .header("Authorization", "Bearer ${HTTP_AUTH_KEY}") - .credentials("HTTP_AUTH_KEY") - .build(); + .name("e2e_s15_authed") + .url("https://api.example.com/secure") + .method("GET") + .header("Authorization", "Bearer ${HTTP_AUTH_KEY}") + .credentials("HTTP_AUTH_KEY") + .build(); Agent agent = Agent.builder() - .name("e2e_s15_authed_agent") - .model(MODEL) - .instructions("Authed HTTP.") - .tools(List.of(http)) - .build(); + .name("e2e_s15_authed_agent") + .model(MODEL) + .instructions("Authed HTTP.") + .tools(List.of(http)) + .build(); Map agentDef = getAgentDef(runtime.plan(agent)); Map tool = findToolByName(agentDef, "e2e_s15_authed"); Map config = (Map) tool.get("config"); List creds = (List) config.get("credentials"); - assertNotNull(creds, - "config.credentials missing on credentialled HTTP tool. config: " + config - + ". COUNTERFACTUAL: if credentials aren't nested, server token won't declare them."); - assertEquals(1, creds.size(), - "Expected 1 credential. Got: " + creds); - assertEquals("HTTP_AUTH_KEY", creds.get(0), - "Credential should be 'HTTP_AUTH_KEY'. Got: " + creds.get(0)); + assertNotNull( + creds, + "config.credentials missing on credentialled HTTP tool. config: " + config + + ". COUNTERFACTUAL: if credentials aren't nested, server token won't declare them."); + assertEquals(1, creds.size(), "Expected 1 credential. Got: " + creds); + assertEquals("HTTP_AUTH_KEY", creds.get(0), "Credential should be 'HTTP_AUTH_KEY'. Got: " + creds.get(0)); // Contrast: unauthenticated tool has no credentials in config ToolDef noAuth = HttpTool.builder() - .name("e2e_s15_unauthed") - .url("https://api.example.com/public") - .method("GET") - .build(); + .name("e2e_s15_unauthed") + .url("https://api.example.com/public") + .method("GET") + .build(); Agent agent2 = Agent.builder() - .name("e2e_s15_unauthed_agent") - .model(MODEL) - .instructions("Public HTTP.") - .tools(List.of(noAuth)) - .build(); + .name("e2e_s15_unauthed_agent") + .model(MODEL) + .instructions("Public HTTP.") + .tools(List.of(noAuth)) + .build(); Map agentDef2 = getAgentDef(runtime.plan(agent2)); Map tool2 = findToolByName(agentDef2, "e2e_s15_unauthed"); Map config2 = (Map) tool2.get("config"); if (config2 != null) { - assertNull(config2.get("credentials"), - "Unauthenticated HTTP tool must have NO credentials in config. Got: " + config2.get("credentials") - + ". COUNTERFACTUAL: if credentials() always added a value, the contrast fails."); + assertNull( + config2.get("credentials"), + "Unauthenticated HTTP tool must have NO credentials in config. Got: " + config2.get("credentials") + + ". COUNTERFACTUAL: if credentials() always added a value, the contrast fails."); } } @@ -275,19 +288,19 @@ void test_http_credentials_nested_in_plan_config() { @SuppressWarnings("unchecked") void test_http_accept_and_content_type_in_plan() { ToolDef http = HttpTool.builder() - .name("e2e_s15_negotiated") - .url("https://api.example.com/data") - .method("POST") - .accept("application/json", "application/xml") - .contentType("application/json") - .build(); + .name("e2e_s15_negotiated") + .url("https://api.example.com/data") + .method("POST") + .accept("application/json", "application/xml") + .contentType("application/json") + .build(); Agent agent = Agent.builder() - .name("e2e_s15_negotiated_agent") - .model(MODEL) - .instructions("Content-negotiated HTTP.") - .tools(List.of(http)) - .build(); + .name("e2e_s15_negotiated_agent") + .model(MODEL) + .instructions("Content-negotiated HTTP.") + .tools(List.of(http)) + .build(); Map agentDef = getAgentDef(runtime.plan(agent)); Map tool = findToolByName(agentDef, "e2e_s15_negotiated"); @@ -296,31 +309,36 @@ void test_http_accept_and_content_type_in_plan() { List accept = (List) config.get("accept"); assertNotNull(accept, "config.accept missing. config: " + config); - assertTrue(accept.contains("application/json") && accept.contains("application/xml"), - "accept should contain both 'application/json' and 'application/xml'. Got: " + accept); + assertTrue( + accept.contains("application/json") && accept.contains("application/xml"), + "accept should contain both 'application/json' and 'application/xml'. Got: " + accept); - assertEquals("application/json", config.get("contentType"), - "config.contentType should be 'application/json'. Got: " + config.get("contentType") - + ". COUNTERFACTUAL: contrast below verifies absence when not set."); + assertEquals( + "application/json", + config.get("contentType"), + "config.contentType should be 'application/json'. Got: " + config.get("contentType") + + ". COUNTERFACTUAL: contrast below verifies absence when not set."); // Contrast: a basic HTTP tool with NO accept/contentType ToolDef plain = HttpTool.builder() - .name("e2e_s15_plain") - .url("https://api.example.com/plain") - .method("GET") - .build(); + .name("e2e_s15_plain") + .url("https://api.example.com/plain") + .method("GET") + .build(); Agent agent2 = Agent.builder() - .name("e2e_s15_plain_agent") - .model(MODEL) - .instructions("Plain HTTP.") - .tools(List.of(plain)) - .build(); + .name("e2e_s15_plain_agent") + .model(MODEL) + .instructions("Plain HTTP.") + .tools(List.of(plain)) + .build(); Map tool2 = findToolByName(getAgentDef(runtime.plan(agent2)), "e2e_s15_plain"); Map config2 = (Map) tool2.get("config"); - assertNull(config2.get("accept"), - "config.accept must be absent when not configured. Got: " + config2.get("accept") - + ". COUNTERFACTUAL: if accept() were always emitted, this would fail."); - assertNull(config2.get("contentType"), - "config.contentType must be absent when not configured. Got: " + config2.get("contentType")); + assertNull( + config2.get("accept"), + "config.accept must be absent when not configured. Got: " + config2.get("accept") + + ". COUNTERFACTUAL: if accept() were always emitted, this would fail."); + assertNull( + config2.get("contentType"), + "config.contentType must be absent when not configured. Got: " + config2.get("contentType")); } } diff --git a/sdk/java/e2e/Suite6PdfTools.java b/sdk/java/e2e/Suite6PdfTools.java index f86986410..c092ff9bb 100644 --- a/sdk/java/e2e/Suite6PdfTools.java +++ b/sdk/java/e2e/Suite6PdfTools.java @@ -1,19 +1,21 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; -import ai.agentspan.tools.PdfTool; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.tools.PdfTool; +import org.junit.jupiter.api.*; /** * Suite 6: PDF Tools — structural plan() assertions for {@link PdfTool}. @@ -33,7 +35,7 @@ class Suite6PdfTools extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -48,13 +50,13 @@ private Map findToolByName(Map agentDef, String List> tools = (List>) agentDef.get("tools"); assertNotNull(tools, "agentDef has no 'tools' key"); return tools.stream() - .filter(t -> name.equals(t.get("name"))) - .findFirst() - .orElseGet(() -> { - fail("Tool '" + name + "' not found. Available: " - + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); - return null; - }); + .filter(t -> name.equals(t.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("Tool '" + name + "' not found. Available: " + + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); + return null; + }); } // ── Tests ───────────────────────────────────────────────────────────────── @@ -71,31 +73,35 @@ private Map findToolByName(Map agentDef, String void test_pdf_tool_def_basic_properties() { ToolDef pdf = PdfTool.create(); - assertEquals("generate_pdf", pdf.getName(), - "Default PDF tool name should be 'generate_pdf'. Got: " + pdf.getName()); - assertEquals("generate_pdf", pdf.getToolType(), - "PDF tool toolType must be 'generate_pdf'. Got: " + pdf.getToolType() - + ". COUNTERFACTUAL: paired with the contrast assertion below."); - assertNull(pdf.getFunc(), - "PDF tool must NOT have a local func — the server runs GENERATE_PDF. Got: " + pdf.getFunc()); - assertNotNull(pdf.getDescription(), - "PDF tool description must be non-null."); - assertTrue(pdf.getDescription().toLowerCase().contains("pdf"), - "PDF tool description should mention 'pdf'. Got: " + pdf.getDescription()); + assertEquals( + "generate_pdf", pdf.getName(), "Default PDF tool name should be 'generate_pdf'. Got: " + pdf.getName()); + assertEquals( + "generate_pdf", + pdf.getToolType(), + "PDF tool toolType must be 'generate_pdf'. Got: " + pdf.getToolType() + + ". COUNTERFACTUAL: paired with the contrast assertion below."); + assertNull( + pdf.getFunc(), + "PDF tool must NOT have a local func — the server runs GENERATE_PDF. Got: " + pdf.getFunc()); + assertNotNull(pdf.getDescription(), "PDF tool description must be non-null."); + assertTrue( + pdf.getDescription().toLowerCase().contains("pdf"), + "PDF tool description should mention 'pdf'. Got: " + pdf.getDescription()); // Counterfactual contrast — a worker ToolDef must NOT be classified as generate_pdf. ToolDef worker = ToolDef.builder() - .name("e2e_s6_contrast_worker") - .description("A worker tool") - .inputSchema(Map.of("type", "object", "properties", Map.of())) - .toolType("worker") - .func(input -> input) - .build(); - assertEquals("worker", worker.getToolType(), - "Worker tool must remain toolType='worker'."); - assertNotEquals(pdf.getToolType(), worker.getToolType(), - "PDF and worker tools must have distinct toolTypes. COUNTERFACTUAL: if PdfTool.create() doesn't " - + "override the default 'worker', these would collide."); + .name("e2e_s6_contrast_worker") + .description("A worker tool") + .inputSchema(Map.of("type", "object", "properties", Map.of())) + .toolType("worker") + .func(input -> input) + .build(); + assertEquals("worker", worker.getToolType(), "Worker tool must remain toolType='worker'."); + assertNotEquals( + pdf.getToolType(), + worker.getToolType(), + "PDF and worker tools must have distinct toolTypes. COUNTERFACTUAL: if PdfTool.create() doesn't " + + "override the default 'worker', these would collide."); } /** @@ -109,18 +115,24 @@ void test_pdf_tool_def_basic_properties() { void test_pdf_tool_custom_name_and_description() { ToolDef custom = PdfTool.create("write_report", "Render a report into PDF."); - assertEquals("write_report", custom.getName(), - "Custom PDF tool name must round-trip. Got: " + custom.getName()); - assertEquals("Render a report into PDF.", custom.getDescription(), - "Custom PDF tool description must round-trip. Got: " + custom.getDescription()); - assertEquals("generate_pdf", custom.getToolType(), - "Custom name must NOT change toolType. Got: " + custom.getToolType()); + assertEquals( + "write_report", custom.getName(), "Custom PDF tool name must round-trip. Got: " + custom.getName()); + assertEquals( + "Render a report into PDF.", + custom.getDescription(), + "Custom PDF tool description must round-trip. Got: " + custom.getDescription()); + assertEquals( + "generate_pdf", + custom.getToolType(), + "Custom name must NOT change toolType. Got: " + custom.getToolType()); // Counterfactual: a second PDF tool with a different name produces a distinct ToolDef. ToolDef other = PdfTool.create("export_pdf", "Export."); - assertNotEquals(custom.getName(), other.getName(), - "Two PDF tools with different names must have distinct ToolDef.name. " - + "COUNTERFACTUAL: if the name parameter were ignored, both would share the same name."); + assertNotEquals( + custom.getName(), + other.getName(), + "Two PDF tools with different names must have distinct ToolDef.name. " + + "COUNTERFACTUAL: if the name parameter were ignored, both would share the same name."); } /** @@ -138,36 +150,42 @@ void test_pdf_tool_default_input_schema() { Map schema = pdf.getInputSchema(); assertNotNull(schema, "Default PDF input schema must not be null."); - assertEquals("object", schema.get("type"), - "Default PDF schema must be a JSON object schema. Got type=" + schema.get("type")); + assertEquals( + "object", + schema.get("type"), + "Default PDF schema must be a JSON object schema. Got type=" + schema.get("type")); Map props = (Map) schema.get("properties"); assertNotNull(props, "Default PDF schema must have 'properties'."); - assertTrue(props.containsKey("markdown"), - "Default PDF schema MUST expose a 'markdown' property. Got keys: " + props.keySet() - + ". COUNTERFACTUAL: without this, the LLM cannot pass markdown content."); + assertTrue( + props.containsKey("markdown"), + "Default PDF schema MUST expose a 'markdown' property. Got keys: " + props.keySet() + + ". COUNTERFACTUAL: without this, the LLM cannot pass markdown content."); List required = (List) schema.get("required"); assertNotNull(required, "Default PDF schema must declare a 'required' list."); - assertTrue(required.contains("markdown"), - "Default PDF schema must require 'markdown'. Got required=" + required - + ". COUNTERFACTUAL: if markdown were optional, calls with empty input would silently produce blank PDFs."); + assertTrue( + required.contains("markdown"), + "Default PDF schema must require 'markdown'. Got required=" + required + + ". COUNTERFACTUAL: if markdown were optional, calls with empty input would silently produce blank PDFs."); // Counterfactual contrast — a custom inputSchema must REPLACE the default, // not be merged with it. Build with a schema that lacks 'markdown'. Map customSchema = Map.of( - "type", "object", - "properties", Map.of("title", Map.of("type", "string")), - "required", List.of("title") - ); + "type", "object", + "properties", Map.of("title", Map.of("type", "string")), + "required", List.of("title")); ToolDef customSchemaPdf = PdfTool.create("titled_pdf", "Title-only PDF.", customSchema); - Map customProps = (Map) customSchemaPdf.getInputSchema().get("properties"); - assertFalse(customProps.containsKey("markdown"), - "Custom inputSchema must REPLACE the default — 'markdown' should be absent. " - + "Got props=" + customProps.keySet() - + ". COUNTERFACTUAL: if the default were always merged in, this would fail."); - assertTrue(customProps.containsKey("title"), - "Custom schema's properties must be preserved. Got: " + customProps.keySet()); + Map customProps = + (Map) customSchemaPdf.getInputSchema().get("properties"); + assertFalse( + customProps.containsKey("markdown"), + "Custom inputSchema must REPLACE the default — 'markdown' should be absent. " + + "Got props=" + customProps.keySet() + + ". COUNTERFACTUAL: if the default were always merged in, this would fail."); + assertTrue( + customProps.containsKey("title"), + "Custom schema's properties must be preserved. Got: " + customProps.keySet()); } /** @@ -184,26 +202,29 @@ void test_pdf_tool_serializes_to_plan() { ToolDef pdf = PdfTool.create(); Agent agent = Agent.builder() - .name("e2e_s6_agent_with_pdf") - .model(MODEL) - .instructions("Generate PDFs.") - .tools(List.of(pdf)) - .build(); + .name("e2e_s6_agent_with_pdf") + .model(MODEL) + .instructions("Generate PDFs.") + .tools(List.of(pdf)) + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); Map tool = findToolByName(agentDef, "generate_pdf"); - assertEquals("generate_pdf", tool.get("toolType"), - "Plan toolType should be 'generate_pdf'. Got: " + tool.get("toolType") - + ". COUNTERFACTUAL: if serializer drops it, server won't dispatch to GENERATE_PDF task."); + assertEquals( + "generate_pdf", + tool.get("toolType"), + "Plan toolType should be 'generate_pdf'. Got: " + tool.get("toolType") + + ". COUNTERFACTUAL: if serializer drops it, server won't dispatch to GENERATE_PDF task."); Map config = (Map) tool.get("config"); - assertNotNull(config, - "PDF tool plan must carry a config (taskType). Got null config."); - assertEquals("GENERATE_PDF", config.get("taskType"), - "config.taskType should be 'GENERATE_PDF'. Got: " + config.get("taskType") - + ". COUNTERFACTUAL: without this, server can't route the tool call to the right system task."); + assertNotNull(config, "PDF tool plan must carry a config (taskType). Got null config."); + assertEquals( + "GENERATE_PDF", + config.get("taskType"), + "config.taskType should be 'GENERATE_PDF'. Got: " + config.get("taskType") + + ". COUNTERFACTUAL: without this, server can't route the tool call to the right system task."); } /** @@ -217,31 +238,34 @@ void test_pdf_tool_serializes_to_plan() { @SuppressWarnings("unchecked") void test_no_pdf_tool_means_no_pdf_in_plan() { ToolDef worker = ToolDef.builder() - .name("e2e_s6_just_worker") - .description("Plain worker.") - .inputSchema(Map.of("type", "object", "properties", Map.of())) - .toolType("worker") - .func(input -> input) - .build(); + .name("e2e_s6_just_worker") + .description("Plain worker.") + .inputSchema(Map.of("type", "object", "properties", Map.of())) + .toolType("worker") + .func(input -> input) + .build(); Agent agent = Agent.builder() - .name("e2e_s6_no_pdf_agent") - .model(MODEL) - .instructions("No PDF here.") - .tools(List.of(worker)) - .build(); + .name("e2e_s6_no_pdf_agent") + .model(MODEL) + .instructions("No PDF here.") + .tools(List.of(worker)) + .build(); - Map plan = runtime.plan(agent); + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); List> tools = (List>) agentDef.get("tools"); assertNotNull(tools, "agentDef.tools is null"); boolean anyPdf = tools.stream().anyMatch(t -> "generate_pdf".equals(t.get("toolType"))); - assertFalse(anyPdf, - "Plan must contain NO toolType='generate_pdf' entries when no PDF tool was added. Got tools: " - + tools.stream().map(t -> t.get("name") + "[" + t.get("toolType") + "]").collect(Collectors.toList()) - + ". COUNTERFACTUAL: if every tool were serialized as generate_pdf, the server would dispatch them all to GENERATE_PDF."); + assertFalse( + anyPdf, + "Plan must contain NO toolType='generate_pdf' entries when no PDF tool was added. Got tools: " + + tools.stream() + .map(t -> t.get("name") + "[" + t.get("toolType") + "]") + .collect(Collectors.toList()) + + ". COUNTERFACTUAL: if every tool were serialized as generate_pdf, the server would dispatch them all to GENERATE_PDF."); } /** @@ -254,48 +278,47 @@ void test_no_pdf_tool_means_no_pdf_in_plan() { @SuppressWarnings("unchecked") void test_pdf_tool_defaults_propagate_to_plan_config() { ToolDef pdfWithDefaults = PdfTool.create( - "fancy_pdf", - "PDF with baked-in defaults.", - null, - Map.of("pageSize", "LETTER", "theme", "compact") - ); + "fancy_pdf", "PDF with baked-in defaults.", null, Map.of("pageSize", "LETTER", "theme", "compact")); Agent agent = Agent.builder() - .name("e2e_s6_defaults_agent") - .model(MODEL) - .instructions("Render with defaults.") - .tools(List.of(pdfWithDefaults)) - .build(); + .name("e2e_s6_defaults_agent") + .model(MODEL) + .instructions("Render with defaults.") + .tools(List.of(pdfWithDefaults)) + .build(); Map agentDef = getAgentDef(runtime.plan(agent)); Map tool = findToolByName(agentDef, "fancy_pdf"); Map config = (Map) tool.get("config"); assertNotNull(config, "Config null on PDF tool with defaults."); - assertEquals("GENERATE_PDF", config.get("taskType"), - "config.taskType should remain 'GENERATE_PDF'. Got: " + config.get("taskType")); - assertEquals("LETTER", config.get("pageSize"), - "config.pageSize should be 'LETTER'. Got: " + config.get("pageSize") - + ". COUNTERFACTUAL: if defaults were dropped, server-side rendering wouldn't honor them."); - assertEquals("compact", config.get("theme"), - "config.theme should be 'compact'. Got: " + config.get("theme")); + assertEquals( + "GENERATE_PDF", + config.get("taskType"), + "config.taskType should remain 'GENERATE_PDF'. Got: " + config.get("taskType")); + assertEquals( + "LETTER", + config.get("pageSize"), + "config.pageSize should be 'LETTER'. Got: " + config.get("pageSize") + + ". COUNTERFACTUAL: if defaults were dropped, server-side rendering wouldn't honor them."); + assertEquals("compact", config.get("theme"), "config.theme should be 'compact'. Got: " + config.get("theme")); // Contrast: PDF tool with NO defaults must NOT have pageSize/theme. ToolDef plain = PdfTool.create("plain_pdf", "No defaults."); Agent agent2 = Agent.builder() - .name("e2e_s6_plain_agent") - .model(MODEL) - .instructions("Plain PDF.") - .tools(List.of(plain)) - .build(); + .name("e2e_s6_plain_agent") + .model(MODEL) + .instructions("Plain PDF.") + .tools(List.of(plain)) + .build(); Map agentDef2 = getAgentDef(runtime.plan(agent2)); Map tool2 = findToolByName(agentDef2, "plain_pdf"); Map config2 = (Map) tool2.get("config"); - assertNull(config2.get("pageSize"), - "Plain PDF tool must have NO pageSize. Got: " + config2.get("pageSize") - + ". COUNTERFACTUAL: if defaults were always emitted, this would fail."); - assertNull(config2.get("theme"), - "Plain PDF tool must have NO theme. Got: " + config2.get("theme")); + assertNull( + config2.get("pageSize"), + "Plain PDF tool must have NO pageSize. Got: " + config2.get("pageSize") + + ". COUNTERFACTUAL: if defaults were always emitted, this would fail."); + assertNull(config2.get("theme"), "Plain PDF tool must have NO theme. Got: " + config2.get("theme")); } /** @@ -318,62 +341,63 @@ void test_pdf_tool_defaults_propagate_to_plan_config() { @Order(7) @SuppressWarnings("unchecked") void test_pdf_generation_task_completes_and_has_output() { - String sampleMarkdown = - "# Agentspan Parity Report\n\n" - + "## Overview\n" - + "This PDF validates the GENERATE_PDF pipeline end-to-end.\n\n" - + "## Numbers\n" - + "- Tests run: 12\n" - + "- Passed: 11\n"; + String sampleMarkdown = "# Agentspan Parity Report\n\n" + + "## Overview\n" + + "This PDF validates the GENERATE_PDF pipeline end-to-end.\n\n" + + "## Numbers\n" + + "- Tests run: 12\n" + + "- Passed: 11\n"; ToolDef pdf = PdfTool.create(); Agent agent = Agent.builder() - .name("e2e_s6_pdf_gen_runtime") - .model(MODEL) - .instructions( - "You generate PDFs. When the user asks, call generate_pdf with the EXACT " - + "markdown they provide. Do not paraphrase, do not summarize.") - .tools(List.of(pdf)) - .build(); + .name("e2e_s6_pdf_gen_runtime") + .model(MODEL) + .instructions("You generate PDFs. When the user asks, call generate_pdf with the EXACT " + + "markdown they provide. Do not paraphrase, do not summarize.") + .tools(List.of(pdf)) + .build(); AgentResult result = runtime.run( - agent, - "Convert the following markdown to a PDF. Pass it exactly to generate_pdf:\n\n" + sampleMarkdown); + agent, + "Convert the following markdown to a PDF. Pass it exactly to generate_pdf:\n\n" + sampleMarkdown); - assertNotNull(result.getExecutionId(), - "Agent run must produce an executionId. status=" + result.getStatus() - + " error=" + result.getError()); - assertTrue(result.isSuccess(), - "Agent run did not succeed: status=" + result.getStatus() - + ", error=" + result.getError()); + assertNotNull( + result.getExecutionId(), + "Agent run must produce an executionId. status=" + result.getStatus() + " error=" + result.getError()); + assertTrue( + result.isSuccess(), + "Agent run did not succeed: status=" + result.getStatus() + ", error=" + result.getError()); Map wf = getWorkflow(result.getExecutionId()); List> tasks = (List>) wf.get("tasks"); assertNotNull(tasks, "Workflow has no tasks array. wfId=" + result.getExecutionId()); Map pdfTask = tasks.stream() - .filter(t -> { - String tt = String.valueOf(t.getOrDefault("taskType", "")); - String tdn = String.valueOf(t.getOrDefault("taskDefName", "")); - return tt.contains("GENERATE_PDF") || tdn.contains("generate_pdf"); - }) - .findFirst() - .orElseGet(() -> { - fail("No GENERATE_PDF task found in workflow. Got task types: " - + tasks.stream().map(t -> t.get("taskType")).collect(Collectors.toList()) - + ". COUNTERFACTUAL: a PdfTool MUST cause the server to dispatch a " - + "GENERATE_PDF system task."); - return null; - }); - - assertEquals("COMPLETED", pdfTask.get("status"), - "GENERATE_PDF task status should be COMPLETED. Got: " + pdfTask.get("status")); + .filter(t -> { + String tt = String.valueOf(t.getOrDefault("taskType", "")); + String tdn = String.valueOf(t.getOrDefault("taskDefName", "")); + return tt.contains("GENERATE_PDF") || tdn.contains("generate_pdf"); + }) + .findFirst() + .orElseGet(() -> { + fail("No GENERATE_PDF task found in workflow. Got task types: " + + tasks.stream().map(t -> t.get("taskType")).collect(Collectors.toList()) + + ". COUNTERFACTUAL: a PdfTool MUST cause the server to dispatch a " + + "GENERATE_PDF system task."); + return null; + }); + + assertEquals( + "COMPLETED", + pdfTask.get("status"), + "GENERATE_PDF task status should be COMPLETED. Got: " + pdfTask.get("status")); Object outputData = pdfTask.get("outputData"); assertNotNull(outputData, "GENERATE_PDF task outputData is null."); String outputStr = outputData.toString(); - assertTrue(outputStr.length() > 20, - "GENERATE_PDF outputData should not be effectively empty. Got: " + outputStr - + ". COUNTERFACTUAL: an empty payload means the renderer ran but produced nothing."); + assertTrue( + outputStr.length() > 20, + "GENERATE_PDF outputData should not be effectively empty. Got: " + outputStr + + ". COUNTERFACTUAL: an empty payload means the renderer ran but produced nothing."); } } diff --git a/sdk/java/e2e/Suite7MediaTools.java b/sdk/java/e2e/Suite7MediaTools.java index dfe47087a..b73e105dc 100644 --- a/sdk/java/e2e/Suite7MediaTools.java +++ b/sdk/java/e2e/Suite7MediaTools.java @@ -1,21 +1,21 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; -import ai.agentspan.tools.MediaTools; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assumptions.assumeTrue; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.tools.MediaTools; +import org.junit.jupiter.api.*; /** * Suite 16: Media Tools — structural plan() assertions for {@link MediaTools}. @@ -36,7 +36,7 @@ class Suite7MediaTools extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -51,13 +51,13 @@ private Map findToolByName(Map agentDef, String List> tools = (List>) agentDef.get("tools"); assertNotNull(tools, "agentDef has no 'tools' key"); return tools.stream() - .filter(t -> name.equals(t.get("name"))) - .findFirst() - .orElseGet(() -> { - fail("Tool '" + name + "' not found. Available: " - + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); - return null; - }); + .filter(t -> name.equals(t.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("Tool '" + name + "' not found. Available: " + + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); + return null; + }); } // ── Tests ───────────────────────────────────────────────────────────────── @@ -73,35 +73,34 @@ private Map findToolByName(Map agentDef, String @Order(1) @SuppressWarnings("unchecked") void test_image_tool_basic_properties() { - ToolDef img = MediaTools.imageTool( - "e2e_s16_image", - "Generate an image.", - "openai", - "dall-e-3"); + ToolDef img = MediaTools.imageTool("e2e_s16_image", "Generate an image.", "openai", "dall-e-3"); assertEquals("e2e_s16_image", img.getName(), "image tool name must round-trip."); - assertEquals("generate_image", img.getToolType(), - "image tool toolType should be 'generate_image'. Got: " + img.getToolType()); + assertEquals( + "generate_image", + img.getToolType(), + "image tool toolType should be 'generate_image'. Got: " + img.getToolType()); Map config = img.getConfig(); assertNotNull(config, "image tool config null"); - assertEquals("openai", config.get("llmProvider"), - "config.llmProvider should be 'openai'. Got: " + config.get("llmProvider")); - assertEquals("dall-e-3", config.get("model"), - "config.model should be 'dall-e-3'. Got: " + config.get("model")); - assertEquals("GENERATE_IMAGE", config.get("taskType"), - "config.taskType should be 'GENERATE_IMAGE'. Got: " + config.get("taskType")); + assertEquals( + "openai", + config.get("llmProvider"), + "config.llmProvider should be 'openai'. Got: " + config.get("llmProvider")); + assertEquals("dall-e-3", config.get("model"), "config.model should be 'dall-e-3'. Got: " + config.get("model")); + assertEquals( + "GENERATE_IMAGE", + config.get("taskType"), + "config.taskType should be 'GENERATE_IMAGE'. Got: " + config.get("taskType")); // Counterfactual contrast — an audioTool must produce a distinct toolType. - ToolDef aud = MediaTools.audioTool( - "e2e_s16_image_contrast_audio", - "Generate audio.", - "openai", - "tts-1"); - assertNotEquals(img.getToolType(), aud.getToolType(), - "imageTool and audioTool must have DIFFERENT toolTypes. Got both='" + img.getToolType() + "'." - + " COUNTERFACTUAL: if every media factory returned the same toolType, the server " - + "would dispatch all media tasks to the same Conductor task."); + ToolDef aud = MediaTools.audioTool("e2e_s16_image_contrast_audio", "Generate audio.", "openai", "tts-1"); + assertNotEquals( + img.getToolType(), + aud.getToolType(), + "imageTool and audioTool must have DIFFERENT toolTypes. Got both='" + img.getToolType() + "'." + + " COUNTERFACTUAL: if every media factory returned the same toolType, the server " + + "would dispatch all media tasks to the same Conductor task."); } /** @@ -112,29 +111,26 @@ void test_image_tool_basic_properties() { @Test @Order(2) void test_audio_tool_basic_properties() { - ToolDef aud = MediaTools.audioTool( - "e2e_s16_audio", - "TTS.", - "openai", - "tts-1"); - - assertEquals("generate_audio", aud.getToolType(), - "audio tool toolType should be 'generate_audio'. Got: " + aud.getToolType()); + ToolDef aud = MediaTools.audioTool("e2e_s16_audio", "TTS.", "openai", "tts-1"); + + assertEquals( + "generate_audio", + aud.getToolType(), + "audio tool toolType should be 'generate_audio'. Got: " + aud.getToolType()); Map config = aud.getConfig(); - assertEquals("tts-1", config.get("model"), - "config.model should be 'tts-1'. Got: " + config.get("model")); - assertEquals("GENERATE_AUDIO", config.get("taskType"), - "config.taskType should be 'GENERATE_AUDIO'. Got: " + config.get("taskType")); + assertEquals("tts-1", config.get("model"), "config.model should be 'tts-1'. Got: " + config.get("model")); + assertEquals( + "GENERATE_AUDIO", + config.get("taskType"), + "config.taskType should be 'GENERATE_AUDIO'. Got: " + config.get("taskType")); // Counterfactual: audio vs video must differ. - ToolDef vid = MediaTools.videoTool( - "e2e_s16_audio_contrast_video", - "Video.", - "openai", - "sora-2"); - assertNotEquals(aud.getToolType(), vid.getToolType(), - "audioTool and videoTool must have DIFFERENT toolTypes. " - + "COUNTERFACTUAL: a stub would make both identical."); + ToolDef vid = MediaTools.videoTool("e2e_s16_audio_contrast_video", "Video.", "openai", "sora-2"); + assertNotEquals( + aud.getToolType(), + vid.getToolType(), + "audioTool and videoTool must have DIFFERENT toolTypes. " + + "COUNTERFACTUAL: a stub would make both identical."); } /** @@ -145,20 +141,23 @@ void test_audio_tool_basic_properties() { @Test @Order(3) void test_video_tool_basic_properties() { - ToolDef vid = MediaTools.videoTool( - "e2e_s16_video", - "Video gen.", - "openai", - "sora-2"); - assertEquals("generate_video", vid.getToolType(), - "video tool toolType should be 'generate_video'. Got: " + vid.getToolType()); - assertEquals("GENERATE_VIDEO", vid.getConfig().get("taskType"), - "config.taskType should be 'GENERATE_VIDEO'. Got: " + vid.getConfig().get("taskType")); + ToolDef vid = MediaTools.videoTool("e2e_s16_video", "Video gen.", "openai", "sora-2"); + assertEquals( + "generate_video", + vid.getToolType(), + "video tool toolType should be 'generate_video'. Got: " + vid.getToolType()); + assertEquals( + "GENERATE_VIDEO", + vid.getConfig().get("taskType"), + "config.taskType should be 'GENERATE_VIDEO'. Got: " + + vid.getConfig().get("taskType")); ToolDef pdf = MediaTools.pdfTool(); - assertNotEquals(vid.getToolType(), pdf.getToolType(), - "videoTool and pdfTool must have DIFFERENT toolTypes. " - + "COUNTERFACTUAL: a stub would collapse them."); + assertNotEquals( + vid.getToolType(), + pdf.getToolType(), + "videoTool and pdfTool must have DIFFERENT toolTypes. " + + "COUNTERFACTUAL: a stub would collapse them."); } /** @@ -172,31 +171,37 @@ void test_video_tool_basic_properties() { @SuppressWarnings("unchecked") void test_pdf_tool_basic_properties() { ToolDef pdf = MediaTools.pdfTool(); - assertEquals("generate_pdf", pdf.getToolType(), - "pdf tool toolType should be 'generate_pdf'. Got: " + pdf.getToolType()); - assertEquals("GENERATE_PDF", pdf.getConfig().get("taskType"), - "config.taskType should be 'GENERATE_PDF'. Got: " + pdf.getConfig().get("taskType")); + assertEquals( + "generate_pdf", + pdf.getToolType(), + "pdf tool toolType should be 'generate_pdf'. Got: " + pdf.getToolType()); + assertEquals( + "GENERATE_PDF", + pdf.getConfig().get("taskType"), + "config.taskType should be 'GENERATE_PDF'. Got: " + + pdf.getConfig().get("taskType")); // Required field check — pdf requires 'markdown' Map schema = pdf.getInputSchema(); List required = (List) schema.get("required"); assertNotNull(required, "pdf inputSchema.required is null"); - assertTrue(required.contains("markdown"), - "pdf required must include 'markdown'. Got: " + required); - assertFalse(required.contains("prompt"), - "pdf required must NOT include 'prompt' (that's for image). Got: " + required - + ". COUNTERFACTUAL: if the schema is shared/cloned, prompt would leak in."); + assertTrue(required.contains("markdown"), "pdf required must include 'markdown'. Got: " + required); + assertFalse( + required.contains("prompt"), + "pdf required must NOT include 'prompt' (that's for image). Got: " + required + + ". COUNTERFACTUAL: if the schema is shared/cloned, prompt would leak in."); // Contrast with image's required field ToolDef img = MediaTools.imageTool("e2e_s16_pdf_contrast_img", "img", "openai", "dall-e-3"); Map imgSchema = img.getInputSchema(); List imgRequired = (List) imgSchema.get("required"); assertNotNull(imgRequired, "image inputSchema.required is null"); - assertTrue(imgRequired.contains("prompt"), - "image required should include 'prompt'. Got: " + imgRequired); - assertNotEquals(required, imgRequired, - "pdf and image required lists must differ. Got pdf=" + required + " img=" + imgRequired - + ". COUNTERFACTUAL: if schemas leak, both would be identical."); + assertTrue(imgRequired.contains("prompt"), "image required should include 'prompt'. Got: " + imgRequired); + assertNotEquals( + required, + imgRequired, + "pdf and image required lists must differ. Got pdf=" + required + " img=" + imgRequired + + ". COUNTERFACTUAL: if schemas leak, both would be identical."); } /** @@ -209,28 +214,17 @@ void test_pdf_tool_basic_properties() { @Order(5) @SuppressWarnings("unchecked") void test_media_tools_serialize_to_plan_with_distinct_models() { - ToolDef img = MediaTools.imageTool( - "e2e_s16_plan_image", - "Image.", - "openai", - "dall-e-3"); + ToolDef img = MediaTools.imageTool("e2e_s16_plan_image", "Image.", "openai", "dall-e-3"); ToolDef gemImg = MediaTools.imageTool( - "e2e_s16_plan_gem_image", - "Image gemini.", - "google_gemini", - "imagen-3.0-generate-002"); - ToolDef aud = MediaTools.audioTool( - "e2e_s16_plan_audio", - "Audio.", - "openai", - "tts-1"); + "e2e_s16_plan_gem_image", "Image gemini.", "google_gemini", "imagen-3.0-generate-002"); + ToolDef aud = MediaTools.audioTool("e2e_s16_plan_audio", "Audio.", "openai", "tts-1"); Agent agent = Agent.builder() - .name("e2e_s16_plan_agent") - .model(MODEL) - .instructions("Generate media.") - .tools(List.of(img, gemImg, aud)) - .build(); + .name("e2e_s16_plan_agent") + .model(MODEL) + .instructions("Generate media.") + .tools(List.of(img, gemImg, aud)) + .build(); Map agentDef = getAgentDef(runtime.plan(agent)); @@ -238,25 +232,36 @@ void test_media_tools_serialize_to_plan_with_distinct_models() { Map gemPlan = findToolByName(agentDef, "e2e_s16_plan_gem_image"); Map audPlan = findToolByName(agentDef, "e2e_s16_plan_audio"); - assertEquals("generate_image", imgPlan.get("toolType"), - "Image plan toolType wrong. Got: " + imgPlan.get("toolType")); - assertEquals("generate_image", gemPlan.get("toolType"), - "Gemini-image plan toolType wrong. Got: " + gemPlan.get("toolType")); - assertEquals("generate_audio", audPlan.get("toolType"), - "Audio plan toolType wrong. Got: " + audPlan.get("toolType") - + ". COUNTERFACTUAL: if every media tool serialized as 'generate_image', this would fail."); + assertEquals( + "generate_image", + imgPlan.get("toolType"), + "Image plan toolType wrong. Got: " + imgPlan.get("toolType")); + assertEquals( + "generate_image", + gemPlan.get("toolType"), + "Gemini-image plan toolType wrong. Got: " + gemPlan.get("toolType")); + assertEquals( + "generate_audio", + audPlan.get("toolType"), + "Audio plan toolType wrong. Got: " + audPlan.get("toolType") + + ". COUNTERFACTUAL: if every media tool serialized as 'generate_image', this would fail."); Map imgConfig = (Map) imgPlan.get("config"); Map gemConfig = (Map) gemPlan.get("config"); - assertEquals("dall-e-3", imgConfig.get("model"), - "OpenAI image model wrong. Got: " + imgConfig.get("model")); - assertEquals("imagen-3.0-generate-002", gemConfig.get("model"), - "Gemini image model wrong. Got: " + gemConfig.get("model")); - assertNotEquals(imgConfig.get("model"), gemConfig.get("model"), - "OpenAI vs Gemini models must differ in plan. " - + "COUNTERFACTUAL: if config.model were dropped or shared, both would match."); - assertNotEquals(imgConfig.get("llmProvider"), gemConfig.get("llmProvider"), - "OpenAI vs Gemini providers must differ. Got both='" + imgConfig.get("llmProvider") + "'."); + assertEquals("dall-e-3", imgConfig.get("model"), "OpenAI image model wrong. Got: " + imgConfig.get("model")); + assertEquals( + "imagen-3.0-generate-002", + gemConfig.get("model"), + "Gemini image model wrong. Got: " + gemConfig.get("model")); + assertNotEquals( + imgConfig.get("model"), + gemConfig.get("model"), + "OpenAI vs Gemini models must differ in plan. " + + "COUNTERFACTUAL: if config.model were dropped or shared, both would match."); + assertNotEquals( + imgConfig.get("llmProvider"), + gemConfig.get("llmProvider"), + "OpenAI vs Gemini providers must differ. Got both='" + imgConfig.get("llmProvider") + "'."); } /** @@ -270,31 +275,34 @@ void test_media_tools_serialize_to_plan_with_distinct_models() { @SuppressWarnings("unchecked") void test_no_media_tool_means_no_generate_in_plan() { ToolDef worker = ToolDef.builder() - .name("e2e_s16_no_media_worker") - .description("worker") - .inputSchema(Map.of("type", "object", "properties", Map.of())) - .toolType("worker") - .func(input -> input) - .build(); + .name("e2e_s16_no_media_worker") + .description("worker") + .inputSchema(Map.of("type", "object", "properties", Map.of())) + .toolType("worker") + .func(input -> input) + .build(); Agent agent = Agent.builder() - .name("e2e_s16_no_media_agent") - .model(MODEL) - .instructions("No media.") - .tools(List.of(worker)) - .build(); + .name("e2e_s16_no_media_agent") + .model(MODEL) + .instructions("No media.") + .tools(List.of(worker)) + .build(); Map agentDef = getAgentDef(runtime.plan(agent)); List> tools = (List>) agentDef.get("tools"); assertNotNull(tools, "tools null"); boolean anyMedia = tools.stream() - .map(t -> (String) t.get("toolType")) - .anyMatch(tt -> tt != null && tt.startsWith("generate_")); - assertFalse(anyMedia, - "Plan must not have generate_* toolType when no media tool was added. Got: " - + tools.stream().map(t -> t.get("name") + "[" + t.get("toolType") + "]").collect(Collectors.toList()) - + ". COUNTERFACTUAL: if media types are always emitted, all plans would have them."); + .map(t -> (String) t.get("toolType")) + .anyMatch(tt -> tt != null && tt.startsWith("generate_")); + assertFalse( + anyMedia, + "Plan must not have generate_* toolType when no media tool was added. Got: " + + tools.stream() + .map(t -> t.get("name") + "[" + t.get("toolType") + "]") + .collect(Collectors.toList()) + + ". COUNTERFACTUAL: if media types are always emitted, all plans would have them."); } /** @@ -315,56 +323,51 @@ void test_no_media_tool_means_no_generate_in_plan() { @SuppressWarnings("unchecked") void test_image_generation_openai_runtime_completes() { String apiKey = System.getenv("OPENAI_API_KEY"); - assumeTrue(apiKey != null && !apiKey.isEmpty(), - "OPENAI_API_KEY not set — skipping live image generation test."); + assumeTrue( + apiKey != null && !apiKey.isEmpty(), "OPENAI_API_KEY not set — skipping live image generation test."); - ToolDef img = MediaTools.imageTool( - "generate_image", - "Generate an image from a text prompt.", - "openai", - "dall-e-3"); + ToolDef img = + MediaTools.imageTool("generate_image", "Generate an image from a text prompt.", "openai", "dall-e-3"); Agent agent = Agent.builder() - .name("e2e_s16_image_openai_runtime") - .model(MODEL) - .instructions( - "You generate images. When the user asks for an image, call generate_image " - + "with the user's prompt.") - .tools(List.of(img)) - .build(); - - AgentResult result = runtime.run( - agent, - "Generate an image of a single red apple on a white background."); - - assertTrue(result.isSuccess(), - "Agent run did not succeed: status=" + result.getStatus() - + ", error=" + result.getError()); + .name("e2e_s16_image_openai_runtime") + .model(MODEL) + .instructions("You generate images. When the user asks for an image, call generate_image " + + "with the user's prompt.") + .tools(List.of(img)) + .build(); + + AgentResult result = runtime.run(agent, "Generate an image of a single red apple on a white background."); + + assertTrue( + result.isSuccess(), + "Agent run did not succeed: status=" + result.getStatus() + ", error=" + result.getError()); Map wf = getWorkflow(result.getExecutionId()); List> tasks = (List>) wf.get("tasks"); assertNotNull(tasks, "Workflow has no tasks. wfId=" + result.getExecutionId()); Map imgTask = tasks.stream() - .filter(t -> { - String tt = String.valueOf(t.getOrDefault("taskType", "")); - String tdn = String.valueOf(t.getOrDefault("taskDefName", "")); - return tt.contains("GENERATE_IMAGE") || tdn.contains("generate_image"); - }) - .findFirst() - .orElseGet(() -> { - fail("No GENERATE_IMAGE task found in workflow. Task types: " - + tasks.stream().map(t -> t.get("taskType")).collect(Collectors.toList()) - + ". COUNTERFACTUAL: an image tool MUST cause the server to dispatch a " - + "GENERATE_IMAGE system task."); - return null; - }); + .filter(t -> { + String tt = String.valueOf(t.getOrDefault("taskType", "")); + String tdn = String.valueOf(t.getOrDefault("taskDefName", "")); + return tt.contains("GENERATE_IMAGE") || tdn.contains("generate_image"); + }) + .findFirst() + .orElseGet(() -> { + fail("No GENERATE_IMAGE task found in workflow. Task types: " + + tasks.stream().map(t -> t.get("taskType")).collect(Collectors.toList()) + + ". COUNTERFACTUAL: an image tool MUST cause the server to dispatch a " + + "GENERATE_IMAGE system task."); + return null; + }); // COMPLETED_WITH_ERRORS is acceptable: the OpenAI image API sometimes returns // soft errors (e.g., moderation warnings) while still producing a valid image; // Conductor surfaces this as COMPLETED_WITH_ERRORS. String status = String.valueOf(imgTask.get("status")); - assertTrue("COMPLETED".equals(status) || "COMPLETED_WITH_ERRORS".equals(status), - "GENERATE_IMAGE task status should be COMPLETED or COMPLETED_WITH_ERRORS. Got: " + status); + assertTrue( + "COMPLETED".equals(status) || "COMPLETED_WITH_ERRORS".equals(status), + "GENERATE_IMAGE task status should be COMPLETED or COMPLETED_WITH_ERRORS. Got: " + status); } } diff --git a/sdk/java/e2e/Suite8Guardrails.java b/sdk/java/e2e/Suite8Guardrails.java index 51a89ceb3..47f86adcd 100644 --- a/sdk/java/e2e/Suite8Guardrails.java +++ b/sdk/java/e2e/Suite8Guardrails.java @@ -1,21 +1,21 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.GuardrailResult; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.concurrent.TimeUnit; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.GuardrailResult; +import org.junit.jupiter.api.*; /** * Suite 3: Guardrails — runtime behavior tests. @@ -46,7 +46,7 @@ class Suite8Guardrails extends BaseTest { static void setup() { // Use BASE_URL (without /api suffix) since AgentConfig + HttpApi // already prepend /api to every path. - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -71,32 +71,31 @@ void test_custom_guardrail_retry_escalation() { // A guardrail that always fails with RETRY and maxRetries=1. // After 1 retry the runtime escalates to RAISE, blocking the agent. GuardrailDef escalatingGuardrail = GuardrailDef.builder() - .name("e2e_escalate_guard") - .position(Position.OUTPUT) - .onFail(OnFail.RETRY) - .maxRetries(1) - .func(content -> GuardrailResult.fail("always fails for escalation test")) - .guardrailType("custom") - .build(); + .name("e2e_escalate_guard") + .position(Position.OUTPUT) + .onFail(OnFail.RETRY) + .maxRetries(1) + .func(content -> GuardrailResult.fail("always fails for escalation test")) + .guardrailType("custom") + .build(); Agent agent = Agent.builder() - .name("e2e_java_escalate_guard_agent") - .model(MODEL) - .instructions("Say hello.") - .guardrails(List.of(escalatingGuardrail)) - .maxTurns(3) - .build(); + .name("e2e_java_escalate_guard_agent") + .model(MODEL) + .instructions("Say hello.") + .guardrails(List.of(escalatingGuardrail)) + .maxTurns(3) + .build(); AgentResult result = runtime.run(agent, "Say anything."); // After maxRetries=1 is exceeded the guardrail escalates to RAISE // which should cause the agent to fail or terminate assertTrue( - result.getStatus() == AgentStatus.FAILED || result.getStatus() == AgentStatus.TERMINATED, - "Expected agent to FAIL or TERMINATE after guardrail maxRetries=1 escalation. " - + "Got status: " + result.getStatus() - + ". COUNTERFACTUAL: if the custom guardrail doesn't fire, agent completes normally." - ); + result.getStatus() == AgentStatus.FAILED || result.getStatus() == AgentStatus.TERMINATED, + "Expected agent to FAIL or TERMINATE after guardrail maxRetries=1 escalation. " + + "Got status: " + result.getStatus() + + ". COUNTERFACTUAL: if the custom guardrail doesn't fire, agent completes normally."); } /** @@ -111,29 +110,28 @@ void test_custom_guardrail_retry_escalation() { void test_custom_guardrail_raise_on_output() { // A guardrail that always blocks output GuardrailDef alwaysBlockGuardrail = GuardrailDef.builder() - .name("e2e_always_block_guard") - .position(Position.OUTPUT) - .onFail(OnFail.RAISE) - .func(content -> GuardrailResult.fail("blocked by e2e test guardrail")) - .guardrailType("custom") - .build(); + .name("e2e_always_block_guard") + .position(Position.OUTPUT) + .onFail(OnFail.RAISE) + .func(content -> GuardrailResult.fail("blocked by e2e test guardrail")) + .guardrailType("custom") + .build(); Agent agent = Agent.builder() - .name("e2e_java_custom_guard_agent") - .model(MODEL) - .instructions("Say hello.") - .guardrails(List.of(alwaysBlockGuardrail)) - .maxTurns(3) - .build(); + .name("e2e_java_custom_guard_agent") + .model(MODEL) + .instructions("Say hello.") + .guardrails(List.of(alwaysBlockGuardrail)) + .maxTurns(3) + .build(); AgentResult result = runtime.run(agent, "Say anything."); // The always-blocking guardrail should cause the agent to fail or terminate assertTrue( - result.getStatus() == AgentStatus.FAILED || result.getStatus() == AgentStatus.TERMINATED, - "Expected agent to FAIL or TERMINATE when custom guardrail always blocks. " - + "Got status: " + result.getStatus() - + ". COUNTERFACTUAL: if the custom guardrail doesn't fire, agent completes normally." - ); + result.getStatus() == AgentStatus.FAILED || result.getStatus() == AgentStatus.TERMINATED, + "Expected agent to FAIL or TERMINATE when custom guardrail always blocks. " + + "Got status: " + result.getStatus() + + ". COUNTERFACTUAL: if the custom guardrail doesn't fire, agent completes normally."); } } diff --git a/sdk/java/e2e/Suite8bGuardrailsExtended.java b/sdk/java/e2e/Suite8bGuardrailsExtended.java index 30160b114..2f699d26a 100644 --- a/sdk/java/e2e/Suite8bGuardrailsExtended.java +++ b/sdk/java/e2e/Suite8bGuardrailsExtended.java @@ -1,19 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.GuardrailResult; -import ai.agentspan.model.ToolDef; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; @@ -21,7 +9,20 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.GuardrailResult; +import org.conductoross.conductor.ai.model.ToolDef; +import org.junit.jupiter.api.*; /** * Suite 5: Extended Guardrails — additional guardrail coverage not in Suite 3. @@ -51,7 +52,7 @@ class Suite8bGuardrailsExtended extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -78,9 +79,8 @@ private Map findGuardrailByName(List> guardr for (Map g : guardrails) { if (name.equals(g.get("name"))) return g; } - List names = guardrails.stream() - .map(g -> (String) g.get("name")) - .collect(Collectors.toList()); + List names = + guardrails.stream().map(g -> (String) g.get("name")).collect(Collectors.toList()); fail("Guardrail '" + name + "' not found in list. Available: " + names); return null; // unreachable } @@ -101,114 +101,121 @@ private Map findGuardrailByName(List> guardr void test_plan_reflects_all_guardrails() { // Agent-level regex INPUT guardrail GuardrailDef regexInputGuard = GuardrailDef.builder() - .name("e2e_regex_input_guard") - .position(Position.INPUT) - .onFail(OnFail.RAISE) - .guardrailType("regex") - .config(Map.of("patterns", List.of("BADWORD"))) - .build(); + .name("e2e_regex_input_guard") + .position(Position.INPUT) + .onFail(OnFail.RAISE) + .guardrailType("regex") + .config(Map.of("patterns", List.of("BADWORD"))) + .build(); // Agent-level regex OUTPUT guardrail (multiple patterns) GuardrailDef regexOutputGuard = GuardrailDef.builder() - .name("e2e_regex_output_guard") - .position(Position.OUTPUT) - .onFail(OnFail.RETRY) - .guardrailType("regex") - .config(Map.of("patterns", List.of("password", "secret"))) - .build(); + .name("e2e_regex_output_guard") + .position(Position.OUTPUT) + .onFail(OnFail.RETRY) + .guardrailType("regex") + .config(Map.of("patterns", List.of("password", "secret"))) + .build(); // Tool-level custom guardrail GuardrailDef toolGuardrail = GuardrailDef.builder() - .name("e2e_tool_guard") - .position(Position.INPUT) - .onFail(OnFail.RAISE) - .guardrailType("custom") - .build(); + .name("e2e_tool_guard") + .position(Position.INPUT) + .onFail(OnFail.RAISE) + .guardrailType("custom") + .build(); ToolDef guardedTool = ToolDef.builder() - .name("e2e_guarded_tool_plan") - .description("A tool with a tool-level guardrail") - .inputSchema(Map.of("type", "object", - "properties", Map.of("input", Map.of("type", "string")))) - .toolType("worker") - .guardrails(List.of(toolGuardrail)) - .build(); + .name("e2e_guarded_tool_plan") + .description("A tool with a tool-level guardrail") + .inputSchema(Map.of("type", "object", "properties", Map.of("input", Map.of("type", "string")))) + .toolType("worker") + .guardrails(List.of(toolGuardrail)) + .build(); Agent agent = Agent.builder() - .name("e2e_java_all_guardrails_agent") - .model(MODEL) - .instructions("You are a test agent.") - .guardrails(List.of(regexInputGuard, regexOutputGuard)) - .tools(List.of(guardedTool)) - .build(); - - Map plan = runtime.plan(agent); + .name("e2e_java_all_guardrails_agent") + .model(MODEL) + .instructions("You are a test agent.") + .guardrails(List.of(regexInputGuard, regexOutputGuard)) + .tools(List.of(guardedTool)) + .build(); + + CompileResponse plan = runtime.plan(agent); Map agentDef = getAgentDef(plan); // ── Assert agent-level guardrails ────────────────────────────── - List> agentGuardrails = - (List>) agentDef.get("guardrails"); - assertNotNull(agentGuardrails, - "agentDef has no 'guardrails' key — agent-level guardrails not serialized"); - assertEquals(2, agentGuardrails.size(), - "Expected 2 agent-level guardrails, got " + agentGuardrails.size() - + ". Guardrails found: " + agentGuardrails.stream() - .map(g -> (String) g.get("name")).collect(Collectors.toList())); + List> agentGuardrails = (List>) agentDef.get("guardrails"); + assertNotNull(agentGuardrails, "agentDef has no 'guardrails' key — agent-level guardrails not serialized"); + assertEquals( + 2, + agentGuardrails.size(), + "Expected 2 agent-level guardrails, got " + agentGuardrails.size() + + ". Guardrails found: " + + agentGuardrails.stream() + .map(g -> (String) g.get("name")) + .collect(Collectors.toList())); // Validate regex INPUT guardrail Map inputGuard = findGuardrailByName(agentGuardrails, "e2e_regex_input_guard"); - assertEquals("regex", inputGuard.get("guardrailType"), - "e2e_regex_input_guard guardrailType should be 'regex', got: " - + inputGuard.get("guardrailType")); - assertEquals("input", inputGuard.get("position"), - "e2e_regex_input_guard position should be 'input', got: " - + inputGuard.get("position")); - assertEquals("raise", inputGuard.get("onFail"), - "e2e_regex_input_guard onFail should be 'raise', got: " - + inputGuard.get("onFail")); + assertEquals( + "regex", + inputGuard.get("guardrailType"), + "e2e_regex_input_guard guardrailType should be 'regex', got: " + inputGuard.get("guardrailType")); + assertEquals( + "input", + inputGuard.get("position"), + "e2e_regex_input_guard position should be 'input', got: " + inputGuard.get("position")); + assertEquals( + "raise", + inputGuard.get("onFail"), + "e2e_regex_input_guard onFail should be 'raise', got: " + inputGuard.get("onFail")); List inputPatterns = (List) inputGuard.get("patterns"); - assertNotNull(inputPatterns, - "e2e_regex_input_guard has no 'patterns' key — config not merged into guardrail map"); - assertTrue(inputPatterns.contains("BADWORD"), - "Expected 'BADWORD' in e2e_regex_input_guard patterns, got: " + inputPatterns); + assertNotNull( + inputPatterns, "e2e_regex_input_guard has no 'patterns' key — config not merged into guardrail map"); + assertTrue( + inputPatterns.contains("BADWORD"), + "Expected 'BADWORD' in e2e_regex_input_guard patterns, got: " + inputPatterns); // Validate regex OUTPUT guardrail Map outputGuard = findGuardrailByName(agentGuardrails, "e2e_regex_output_guard"); - assertEquals("regex", outputGuard.get("guardrailType"), - "e2e_regex_output_guard guardrailType should be 'regex', got: " - + outputGuard.get("guardrailType")); - assertEquals("output", outputGuard.get("position"), - "e2e_regex_output_guard position should be 'output', got: " - + outputGuard.get("position")); - assertEquals("retry", outputGuard.get("onFail"), - "e2e_regex_output_guard onFail should be 'retry', got: " - + outputGuard.get("onFail")); + assertEquals( + "regex", + outputGuard.get("guardrailType"), + "e2e_regex_output_guard guardrailType should be 'regex', got: " + outputGuard.get("guardrailType")); + assertEquals( + "output", + outputGuard.get("position"), + "e2e_regex_output_guard position should be 'output', got: " + outputGuard.get("position")); + assertEquals( + "retry", + outputGuard.get("onFail"), + "e2e_regex_output_guard onFail should be 'retry', got: " + outputGuard.get("onFail")); // ── Assert tool-level guardrail ──────────────────────────────── List> tools = (List>) agentDef.get("tools"); assertNotNull(tools, "agentDef has no 'tools' key"); Map foundTool = tools.stream() - .filter(t -> "e2e_guarded_tool_plan".equals(t.get("name"))) - .findFirst() - .orElse(null); - assertNotNull(foundTool, - "Tool 'e2e_guarded_tool_plan' not found in agentDef.tools"); - - List> toolGuardrails = - (List>) foundTool.get("guardrails"); - assertNotNull(toolGuardrails, - "Tool 'e2e_guarded_tool_plan' has no 'guardrails' key — tool-level guardrail not serialized"); - assertFalse(toolGuardrails.isEmpty(), - "Tool 'e2e_guarded_tool_plan'.guardrails list is empty"); + .filter(t -> "e2e_guarded_tool_plan".equals(t.get("name"))) + .findFirst() + .orElse(null); + assertNotNull(foundTool, "Tool 'e2e_guarded_tool_plan' not found in agentDef.tools"); + + List> toolGuardrails = (List>) foundTool.get("guardrails"); + assertNotNull( + toolGuardrails, + "Tool 'e2e_guarded_tool_plan' has no 'guardrails' key — tool-level guardrail not serialized"); + assertFalse(toolGuardrails.isEmpty(), "Tool 'e2e_guarded_tool_plan'.guardrails list is empty"); Map tg = findGuardrailByName(toolGuardrails, "e2e_tool_guard"); - assertEquals("input", tg.get("position"), - "e2e_tool_guard position should be 'input', got: " + tg.get("position")); - assertEquals("raise", tg.get("onFail"), - "e2e_tool_guard onFail should be 'raise', got: " + tg.get("onFail")); - assertEquals("custom", tg.get("guardrailType"), - "e2e_tool_guard guardrailType should be 'custom', got: " + tg.get("guardrailType")); + assertEquals( + "input", tg.get("position"), "e2e_tool_guard position should be 'input', got: " + tg.get("position")); + assertEquals("raise", tg.get("onFail"), "e2e_tool_guard onFail should be 'raise', got: " + tg.get("onFail")); + assertEquals( + "custom", + tg.get("guardrailType"), + "e2e_tool_guard guardrailType should be 'custom', got: " + tg.get("guardrailType")); } /** @@ -237,41 +244,44 @@ void test_passing_guardrail_does_not_block_tool_execution() { toolBodyExecuted.set(false); GuardrailDef alwaysPassGuard = GuardrailDef.builder() - .name("e2e_always_pass_guard") - .position(Position.OUTPUT) - .onFail(OnFail.RAISE) - .func(content -> GuardrailResult.pass()) - .guardrailType("custom") - .build(); + .name("e2e_always_pass_guard") + .position(Position.OUTPUT) + .onFail(OnFail.RAISE) + .func(content -> GuardrailResult.pass()) + .guardrailType("custom") + .build(); Agent agent = Agent.builder() - .name("e2e_java_passing_guard_agent") - .model(MODEL) - .instructions("You MUST call the e2e_tracked_tool tool with argument input='hello'. " - + "Call it exactly once and then respond with the result.") - .tools(ToolRegistry.fromInstance(new TrackedTools())) - .guardrails(List.of(alwaysPassGuard)) - .maxTurns(3) - .build(); + .name("e2e_java_passing_guard_agent") + .model(MODEL) + .instructions("You MUST call the e2e_tracked_tool tool with argument input='hello'. " + + "Call it exactly once and then respond with the result.") + .tools(ToolRegistry.fromInstance(new TrackedTools())) + .guardrails(List.of(alwaysPassGuard)) + .maxTurns(3) + .build(); AgentResult result = runtime.run(agent, "Call the tool with input hello."); // The tool should have executed - assertTrue(toolBodyExecuted.get(), - "The 'e2e_tracked_tool' function body was never called. " - + "COUNTERFACTUAL B: if tool registration or dispatch is broken, " - + "the tool is never invoked and this flag stays false."); + assertTrue( + toolBodyExecuted.get(), + "The 'e2e_tracked_tool' function body was never called. " + + "COUNTERFACTUAL B: if tool registration or dispatch is broken, " + + "the tool is never invoked and this flag stays false."); // The passing guardrail should NOT block completion - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Expected COMPLETED when guardrail always passes. " - + "Got status: " + result.getStatus() - + ". COUNTERFACTUAL A: if the guardrail incorrectly blocks, status != COMPLETED."); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Expected COMPLETED when guardrail always passes. " + + "Got status: " + result.getStatus() + + ". COUNTERFACTUAL A: if the guardrail incorrectly blocks, status != COMPLETED."); } /** Side-effect flag for test_passing_guardrail_does_not_block_tool_execution. */ private static final java.util.concurrent.atomic.AtomicBoolean toolBodyExecuted = - new java.util.concurrent.atomic.AtomicBoolean(false); + new java.util.concurrent.atomic.AtomicBoolean(false); static class TrackedTools { @Tool(name = "e2e_tracked_tool", description = "Echoes the input; sets a side-effect flag") @@ -308,40 +318,41 @@ void test_tool_output_detected_by_guardrail() { sqlToolBodyRan.set(false); GuardrailDef markerGuard = GuardrailDef.builder() - .name("e2e_marker_output_guard") - .position(Position.OUTPUT) - .onFail(OnFail.RAISE) - .func(content -> content.contains("BLOCKED_MARKER") - ? GuardrailResult.fail("blocked: marker detected in output") - : GuardrailResult.pass()) - .guardrailType("custom") - .build(); + .name("e2e_marker_output_guard") + .position(Position.OUTPUT) + .onFail(OnFail.RAISE) + .func(content -> content.contains("BLOCKED_MARKER") + ? GuardrailResult.fail("blocked: marker detected in output") + : GuardrailResult.pass()) + .guardrailType("custom") + .build(); Agent agent = Agent.builder() - .name("e2e_java_marker_guard_agent") - .model(MODEL) - .instructions("You MUST call the e2e_marker_tool tool with input='test'. " - + "Then repeat the tool result verbatim in your response.") - .tools(ToolRegistry.fromInstance(new MarkerTools())) - .guardrails(List.of(markerGuard)) - .requiredTools("e2e_marker_tool") - .maxTurns(5) - .build(); + .name("e2e_java_marker_guard_agent") + .model(MODEL) + .instructions("You MUST call the e2e_marker_tool tool with input='test'. " + + "Then repeat the tool result verbatim in your response.") + .tools(ToolRegistry.fromInstance(new MarkerTools())) + .guardrails(List.of(markerGuard)) + .requiredTools("e2e_marker_tool") + .maxTurns(5) + .build(); AgentResult result = runtime.run(agent, "Call e2e_marker_tool with input='test' and repeat the result."); // The tool body MUST have executed (requiredTools guarantees it) - assertTrue(sqlToolBodyRan.get(), - "The 'e2e_marker_tool' body was never called. " - + "COUNTERFACTUAL B: if tool registration or dispatch is broken, the tool is never " - + "invoked and this flag stays false."); + assertTrue( + sqlToolBodyRan.get(), + "The 'e2e_marker_tool' body was never called. " + + "COUNTERFACTUAL B: if tool registration or dispatch is broken, the tool is never " + + "invoked and this flag stays false."); // The guardrail must have detected "BLOCKED_MARKER" and blocked the agent assertTrue( - result.getStatus() == AgentStatus.FAILED || result.getStatus() == AgentStatus.TERMINATED, - "Expected agent to FAIL or TERMINATE when OUTPUT guardrail detects BLOCKED_MARKER. " - + "Got status: " + result.getStatus() - + ". COUNTERFACTUAL A: if output guardrail doesn't detect the marker, agent completes normally."); + result.getStatus() == AgentStatus.FAILED || result.getStatus() == AgentStatus.TERMINATED, + "Expected agent to FAIL or TERMINATE when OUTPUT guardrail detects BLOCKED_MARKER. " + + "Got status: " + result.getStatus() + + ". COUNTERFACTUAL A: if output guardrail doesn't detect the marker, agent completes normally."); } /** Side-effect flag for test_tool_output_detected_by_guardrail. */ @@ -371,29 +382,28 @@ public String marker(String input) { @Timeout(value = 300, unit = TimeUnit.SECONDS) void test_max_retries_escalation() { GuardrailDef alwaysRetryGuard = GuardrailDef.builder() - .name("e2e_suite5_retry_guard") - .position(Position.OUTPUT) - .onFail(OnFail.RETRY) - .maxRetries(1) - .func(content -> GuardrailResult.fail("always fails — retry escalation test")) - .guardrailType("custom") - .build(); + .name("e2e_suite5_retry_guard") + .position(Position.OUTPUT) + .onFail(OnFail.RETRY) + .maxRetries(1) + .func(content -> GuardrailResult.fail("always fails — retry escalation test")) + .guardrailType("custom") + .build(); Agent agent = Agent.builder() - .name("e2e_java_suite5_retry_agent") - .model(MODEL) - .instructions("Say hello.") - .guardrails(List.of(alwaysRetryGuard)) - .maxTurns(3) - .build(); + .name("e2e_java_suite5_retry_agent") + .model(MODEL) + .instructions("Say hello.") + .guardrails(List.of(alwaysRetryGuard)) + .maxTurns(3) + .build(); AgentResult result = runtime.run(agent, "Say anything."); assertTrue( - result.getStatus() == AgentStatus.FAILED || result.getStatus() == AgentStatus.TERMINATED, - "Expected agent to FAIL or TERMINATE after guardrail maxRetries=1 escalation. " - + "Got status: " + result.getStatus() - + ". COUNTERFACTUAL: if maxRetries escalation is broken, agent completes or loops forever." - ); + result.getStatus() == AgentStatus.FAILED || result.getStatus() == AgentStatus.TERMINATED, + "Expected agent to FAIL or TERMINATE after guardrail maxRetries=1 escalation. " + + "Got status: " + result.getStatus() + + ". COUNTERFACTUAL: if maxRetries escalation is broken, agent completes or loops forever."); } } diff --git a/sdk/java/e2e/Suite9Handoffs.java b/sdk/java/e2e/Suite9Handoffs.java index 3e4e692aa..423960658 100644 --- a/sdk/java/e2e/Suite9Handoffs.java +++ b/sdk/java/e2e/Suite9Handoffs.java @@ -1,21 +1,21 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.enums.Strategy; -import ai.agentspan.handoff.OnTextMention; -import ai.agentspan.model.AgentResult; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.handoff.OnTextMention; +import org.conductoross.conductor.ai.model.AgentResult; +import org.junit.jupiter.api.*; /** * Suite 6: Handoffs — multi-agent strategy runtime tests. @@ -42,7 +42,7 @@ class Suite9Handoffs extends BaseTest { @BeforeAll static void setup() { - runtime = new AgentRuntime(new ai.agentspan.AgentConfig(BASE_URL, null, null, 100, 1)); + runtime = new AgentRuntime(new AgentConfig(100, 1)); } @AfterAll @@ -54,18 +54,18 @@ static void teardown() { static Agent mathAgent() { return Agent.builder() - .name("e2e_java_math") - .model(MODEL) - .instructions("You are a math agent. Compute arithmetic. Be concise.") - .build(); + .name("e2e_java_math") + .model(MODEL) + .instructions("You are a math agent. Compute arithmetic. Be concise.") + .build(); } static Agent textAgent() { return Agent.builder() - .name("e2e_java_text") - .model(MODEL) - .instructions("You are a text agent. Process text. Be concise.") - .build(); + .name("e2e_java_text") + .model(MODEL) + .instructions("You are a text agent. Process text. Be concise.") + .build(); } // ── Tests ───────────────────────────────────────────────────────────── @@ -82,19 +82,21 @@ static Agent textAgent() { @SuppressWarnings("unchecked") void test_sequential_execution() { Agent parent = Agent.builder() - .name("e2e_java_sequential_parent") - .model(MODEL) - .instructions("Delegate tasks sequentially to your sub-agents.") - .agents(mathAgent(), textAgent()) - .strategy(Strategy.SEQUENTIAL) - .build(); + .name("e2e_java_sequential_parent") + .model(MODEL) + .instructions("Delegate tasks sequentially to your sub-agents.") + .agents(mathAgent(), textAgent()) + .strategy(Strategy.SEQUENTIAL) + .build(); AgentResult result = runtime.run(parent, "Compute 3+4, then reverse the word hello"); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "SEQUENTIAL parent agent should complete. " - + "Status: " + result.getStatus() - + ". Error: " + result.getError()); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "SEQUENTIAL parent agent should complete. " + + "Status: " + result.getStatus() + + ". Error: " + result.getError()); String executionId = result.getExecutionId(); assertNotNull(executionId, "executionId is null"); @@ -106,22 +108,23 @@ void test_sequential_execution() { List> tasks = (List>) workflow.get("tasks"); assertNotNull(tasks, "workflow has neither 'workflowDef' nor 'tasks'"); long subWorkflowCount = tasks.stream() - .filter(t -> "SUB_WORKFLOW".equals(t.get("taskType")) - || "SUB_WORKFLOW".equals(t.get("type"))) - .count(); - assertTrue(subWorkflowCount >= 2, - "Expected at least 2 SUB_WORKFLOW tasks for SEQUENTIAL execution, found " - + subWorkflowCount - + ". COUNTERFACTUAL: if only 1 agent ran, count < 2."); + .filter(t -> "SUB_WORKFLOW".equals(t.get("taskType")) || "SUB_WORKFLOW".equals(t.get("type"))) + .count(); + assertTrue( + subWorkflowCount >= 2, + "Expected at least 2 SUB_WORKFLOW tasks for SEQUENTIAL execution, found " + + subWorkflowCount + + ". COUNTERFACTUAL: if only 1 agent ran, count < 2."); } else { List> allTasks = allTasksFlat(workflowDef); long subWorkflowCount = allTasks.stream() - .filter(t -> "SUB_WORKFLOW".equals(t.get("type"))) - .count(); - assertTrue(subWorkflowCount >= 2, - "Expected at least 2 SUB_WORKFLOW tasks in SEQUENTIAL plan, found " - + subWorkflowCount - + ". COUNTERFACTUAL: if SEQUENTIAL strategy only serializes 1 agent, count < 2."); + .filter(t -> "SUB_WORKFLOW".equals(t.get("type"))) + .count(); + assertTrue( + subWorkflowCount >= 2, + "Expected at least 2 SUB_WORKFLOW tasks in SEQUENTIAL plan, found " + + subWorkflowCount + + ". COUNTERFACTUAL: if SEQUENTIAL strategy only serializes 1 agent, count < 2."); } } @@ -137,19 +140,21 @@ void test_sequential_execution() { @SuppressWarnings("unchecked") void test_parallel_execution() { Agent parent = Agent.builder() - .name("e2e_java_parallel_parent") - .model(MODEL) - .instructions("Run both sub-agents in parallel.") - .agents(mathAgent(), textAgent()) - .strategy(Strategy.PARALLEL) - .build(); + .name("e2e_java_parallel_parent") + .model(MODEL) + .instructions("Run both sub-agents in parallel.") + .agents(mathAgent(), textAgent()) + .strategy(Strategy.PARALLEL) + .build(); AgentResult result = runtime.run(parent, "Compute 3+4 AND reverse the word hello"); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "PARALLEL parent agent should complete. " - + "Status: " + result.getStatus() - + ". Error: " + result.getError()); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "PARALLEL parent agent should complete. " + + "Status: " + result.getStatus() + + ". Error: " + result.getError()); String executionId = result.getExecutionId(); assertNotNull(executionId, "executionId is null"); @@ -160,31 +165,34 @@ void test_parallel_execution() { Map workflowDef = (Map) workflow.get("workflowDef"); if (workflowDef != null) { List> allTasks = allTasksFlat(workflowDef); - boolean hasFork = allTasks.stream() - .anyMatch(t -> "FORK_JOIN".equals(t.get("type")) - || "FORK".equals(t.get("type"))); - assertTrue(hasFork, - "Expected a FORK_JOIN or FORK task in PARALLEL workflow plan. " - + "Task types found: " + allTasks.stream() - .map(t -> (String) t.get("type")).collect(Collectors.toSet()) - + ". COUNTERFACTUAL: if PARALLEL degrades to sequential, no FORK task appears."); + boolean hasFork = + allTasks.stream().anyMatch(t -> "FORK_JOIN".equals(t.get("type")) || "FORK".equals(t.get("type"))); + assertTrue( + hasFork, + "Expected a FORK_JOIN or FORK task in PARALLEL workflow plan. " + + "Task types found: " + + allTasks.stream().map(t -> (String) t.get("type")).collect(Collectors.toSet()) + + ". COUNTERFACTUAL: if PARALLEL degrades to sequential, no FORK task appears."); } else { // Fall back to execution tasks List> tasks = (List>) workflow.get("tasks"); assertNotNull(tasks, "workflow has neither 'workflowDef' nor 'tasks'"); boolean hasFork = tasks.stream() - .anyMatch(t -> "FORK_JOIN".equals(t.get("taskType")) - || "FORK_JOIN".equals(t.get("type")) - || "FORK".equals(t.get("taskType")) - || "FORK".equals(t.get("type"))); - assertTrue(hasFork, - "Expected a FORK_JOIN or FORK task in PARALLEL workflow execution. " - + "Task types found: " + tasks.stream() - .map(t -> { - String tt = (String) t.get("taskType"); - return tt != null ? tt : (String) t.get("type"); - }).collect(Collectors.toSet()) - + ". COUNTERFACTUAL: if PARALLEL degrades to sequential, no FORK task appears."); + .anyMatch(t -> "FORK_JOIN".equals(t.get("taskType")) + || "FORK_JOIN".equals(t.get("type")) + || "FORK".equals(t.get("taskType")) + || "FORK".equals(t.get("type"))); + assertTrue( + hasFork, + "Expected a FORK_JOIN or FORK task in PARALLEL workflow execution. " + + "Task types found: " + + tasks.stream() + .map(t -> { + String tt = (String) t.get("taskType"); + return tt != null ? tt : (String) t.get("type"); + }) + .collect(Collectors.toSet()) + + ". COUNTERFACTUAL: if PARALLEL degrades to sequential, no FORK task appears."); } } @@ -200,19 +208,21 @@ void test_parallel_execution() { @SuppressWarnings("unchecked") void test_handoff_execution() { Agent parent = Agent.builder() - .name("e2e_java_handoff_parent") - .model(MODEL) - .instructions("You are a coordinator. Hand off text processing tasks to the text agent.") - .agents(mathAgent(), textAgent()) - .strategy(Strategy.HANDOFF) - .build(); + .name("e2e_java_handoff_parent") + .model(MODEL) + .instructions("You are a coordinator. Hand off text processing tasks to the text agent.") + .agents(mathAgent(), textAgent()) + .strategy(Strategy.HANDOFF) + .build(); AgentResult result = runtime.run(parent, "Reverse the word hello"); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "HANDOFF parent agent should complete. " - + "Status: " + result.getStatus() - + ". Error: " + result.getError()); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "HANDOFF parent agent should complete. " + + "Status: " + result.getStatus() + + ". Error: " + result.getError()); String executionId = result.getExecutionId(); assertNotNull(executionId, "executionId is null"); @@ -223,24 +233,24 @@ void test_handoff_execution() { Map workflowDef = (Map) workflow.get("workflowDef"); if (workflowDef != null) { List> allTasks = allTasksFlat(workflowDef); - boolean hasSubWorkflow = allTasks.stream() - .anyMatch(t -> "SUB_WORKFLOW".equals(t.get("type"))); - assertTrue(hasSubWorkflow, - "Expected at least 1 SUB_WORKFLOW task in HANDOFF workflow plan. " - + "Task types found: " + allTasks.stream() - .map(t -> (String) t.get("type")).collect(Collectors.toSet()) - + ". COUNTERFACTUAL: if HANDOFF never creates sub-workflows, this fails."); + boolean hasSubWorkflow = allTasks.stream().anyMatch(t -> "SUB_WORKFLOW".equals(t.get("type"))); + assertTrue( + hasSubWorkflow, + "Expected at least 1 SUB_WORKFLOW task in HANDOFF workflow plan. " + + "Task types found: " + + allTasks.stream().map(t -> (String) t.get("type")).collect(Collectors.toSet()) + + ". COUNTERFACTUAL: if HANDOFF never creates sub-workflows, this fails."); } else { List> tasks = (List>) workflow.get("tasks"); assertNotNull(tasks, "workflow has neither 'workflowDef' nor 'tasks'"); long subWorkflowCount = tasks.stream() - .filter(t -> "SUB_WORKFLOW".equals(t.get("taskType")) - || "SUB_WORKFLOW".equals(t.get("type"))) - .count(); - assertTrue(subWorkflowCount >= 1, - "Expected at least 1 SUB_WORKFLOW task in HANDOFF execution, found " - + subWorkflowCount - + ". COUNTERFACTUAL: if HANDOFF never creates a sub-workflow, count = 0."); + .filter(t -> "SUB_WORKFLOW".equals(t.get("taskType")) || "SUB_WORKFLOW".equals(t.get("type"))) + .count(); + assertTrue( + subWorkflowCount >= 1, + "Expected at least 1 SUB_WORKFLOW task in HANDOFF execution, found " + + subWorkflowCount + + ". COUNTERFACTUAL: if HANDOFF never creates a sub-workflow, count = 0."); } } @@ -260,39 +270,43 @@ void test_handoff_execution() { @SuppressWarnings("unchecked") void test_pipe_operator_then() { Agent math = Agent.builder() - .name("e2e_java_pipe_math") - .model(MODEL) - .instructions("Compute arithmetic. Be concise.") - .build(); + .name("e2e_java_pipe_math") + .model(MODEL) + .instructions("Compute arithmetic. Be concise.") + .build(); Agent text = Agent.builder() - .name("e2e_java_pipe_text") - .model(MODEL) - .instructions("Process text. Be concise.") - .build(); + .name("e2e_java_pipe_text") + .model(MODEL) + .instructions("Process text. Be concise.") + .build(); // ── Structural assertion (no server) ────────────────────────── Agent pipeline = math.then(text); - assertEquals(Strategy.SEQUENTIAL, pipeline.getStrategy(), - "Agent.then() should produce Strategy.SEQUENTIAL, got: " + pipeline.getStrategy() - + ". COUNTERFACTUAL: if .then() uses wrong strategy, this fails."); + assertEquals( + Strategy.SEQUENTIAL, + pipeline.getStrategy(), + "Agent.then() should produce Strategy.SEQUENTIAL, got: " + pipeline.getStrategy() + + ". COUNTERFACTUAL: if .then() uses wrong strategy, this fails."); List pipelineAgents = pipeline.getAgents(); - List agentNames = pipelineAgents.stream() - .map(Agent::getName) - .collect(Collectors.toList()); - assertTrue(agentNames.contains("e2e_java_pipe_math"), - "Pipeline missing 'e2e_java_pipe_math'. Found: " + agentNames); - assertTrue(agentNames.contains("e2e_java_pipe_text"), - "Pipeline missing 'e2e_java_pipe_text'. Found: " + agentNames); + List agentNames = pipelineAgents.stream().map(Agent::getName).collect(Collectors.toList()); + assertTrue( + agentNames.contains("e2e_java_pipe_math"), + "Pipeline missing 'e2e_java_pipe_math'. Found: " + agentNames); + assertTrue( + agentNames.contains("e2e_java_pipe_text"), + "Pipeline missing 'e2e_java_pipe_text'. Found: " + agentNames); // ── Runtime assertion ───────────────────────────────────────── AgentResult result = runtime.run(pipeline, "Compute 2+2 and reverse hello"); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "Pipeline via .then() should complete. " - + "Status: " + result.getStatus() - + ". Error: " + result.getError()); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "Pipeline via .then() should complete. " + + "Status: " + result.getStatus() + + ". Error: " + result.getError()); String executionId = result.getExecutionId(); assertNotNull(executionId, "executionId is null"); @@ -303,23 +317,24 @@ void test_pipe_operator_then() { if (workflowDef != null) { List> allTasks = allTasksFlat(workflowDef); long subWorkflowCount = allTasks.stream() - .filter(t -> "SUB_WORKFLOW".equals(t.get("type"))) - .count(); - assertTrue(subWorkflowCount >= 2, - "Expected at least 2 SUB_WORKFLOW tasks for .then() pipeline, found " - + subWorkflowCount - + ". COUNTERFACTUAL: if only 1 agent ran, count < 2."); + .filter(t -> "SUB_WORKFLOW".equals(t.get("type"))) + .count(); + assertTrue( + subWorkflowCount >= 2, + "Expected at least 2 SUB_WORKFLOW tasks for .then() pipeline, found " + + subWorkflowCount + + ". COUNTERFACTUAL: if only 1 agent ran, count < 2."); } else { List> tasks = (List>) workflow.get("tasks"); assertNotNull(tasks, "workflow has neither 'workflowDef' nor 'tasks'"); long subWorkflowCount = tasks.stream() - .filter(t -> "SUB_WORKFLOW".equals(t.get("taskType")) - || "SUB_WORKFLOW".equals(t.get("type"))) - .count(); - assertTrue(subWorkflowCount >= 2, - "Expected at least 2 SUB_WORKFLOW tasks for .then() pipeline, found " - + subWorkflowCount - + ". COUNTERFACTUAL: if only 1 agent ran, count < 2."); + .filter(t -> "SUB_WORKFLOW".equals(t.get("taskType")) || "SUB_WORKFLOW".equals(t.get("type"))) + .count(); + assertTrue( + subWorkflowCount >= 2, + "Expected at least 2 SUB_WORKFLOW tasks for .then() pipeline, found " + + subWorkflowCount + + ". COUNTERFACTUAL: if only 1 agent ran, count < 2."); } } @@ -336,26 +351,29 @@ void test_pipe_operator_then() { @SuppressWarnings("unchecked") void test_router_selects_correct_agent() { Agent routerLead = Agent.builder() - .name("e2e_java_router_lead_agent") - .model(MODEL) - .instructions("You are a router. Route math requests to e2e_java_math and text requests to e2e_java_text.") - .build(); + .name("e2e_java_router_lead_agent") + .model(MODEL) + .instructions( + "You are a router. Route math requests to e2e_java_math and text requests to e2e_java_text.") + .build(); Agent parent = Agent.builder() - .name("e2e_java_router_parent") - .model(MODEL) - .instructions("Route requests to the appropriate sub-agent using the router.") - .agents(mathAgent(), textAgent()) - .strategy(Strategy.ROUTER) - .router(routerLead) - .build(); + .name("e2e_java_router_parent") + .model(MODEL) + .instructions("Route requests to the appropriate sub-agent using the router.") + .agents(mathAgent(), textAgent()) + .strategy(Strategy.ROUTER) + .router(routerLead) + .build(); AgentResult result = runtime.run(parent, "Compute 7 times 8"); - assertEquals(AgentStatus.COMPLETED, result.getStatus(), - "ROUTER parent agent should complete. " - + "Status: " + result.getStatus() - + ". Error: " + result.getError()); + assertEquals( + AgentStatus.COMPLETED, + result.getStatus(), + "ROUTER parent agent should complete. " + + "Status: " + result.getStatus() + + ". Error: " + result.getError()); String executionId = result.getExecutionId(); assertNotNull(executionId, "executionId is null"); @@ -366,19 +384,21 @@ void test_router_selects_correct_agent() { List> allExecTasks = (List>) workflow.get("tasks"); assertNotNull(allExecTasks, "workflow has no 'tasks' field"); - boolean mathSubWorkflowFound = allExecTasks.stream() - .anyMatch(t -> { - String taskType = (String) t.getOrDefault("taskType", ""); - String ref = (String) t.getOrDefault("referenceTaskName", ""); - return "SUB_WORKFLOW".equals(taskType) && ref.contains("math"); - }); - - assertTrue(mathSubWorkflowFound, - "Expected at least one SUB_WORKFLOW task with referenceTaskName containing 'math'. " - + "COUNTERFACTUAL: if router doesn't route to math_agent, no math sub-workflow appears. " - + "Execution tasks found: " + allExecTasks.stream() - .map(t -> t.getOrDefault("taskType", "?") + ":" + t.getOrDefault("referenceTaskName", "?")) - .collect(Collectors.toList())); + boolean mathSubWorkflowFound = allExecTasks.stream().anyMatch(t -> { + String taskType = (String) t.getOrDefault("taskType", ""); + String ref = (String) t.getOrDefault("referenceTaskName", ""); + return "SUB_WORKFLOW".equals(taskType) && ref.contains("math"); + }); + + assertTrue( + mathSubWorkflowFound, + "Expected at least one SUB_WORKFLOW task with referenceTaskName containing 'math'. " + + "COUNTERFACTUAL: if router doesn't route to math_agent, no math sub-workflow appears. " + + "Execution tasks found: " + + allExecTasks.stream() + .map(t -> t.getOrDefault("taskType", "?") + ":" + + t.getOrDefault("referenceTaskName", "?")) + .collect(Collectors.toList())); } /** @@ -394,26 +414,24 @@ void test_router_selects_correct_agent() { @SuppressWarnings("unchecked") void test_swarm_with_text_mention() { Agent parent = Agent.builder() - .name("e2e_java_swarm_parent") - .model(MODEL) - .instructions("You are a coordinator. When asked to reverse text, mention 'reverse' to route to the text agent.") - .agents(mathAgent(), textAgent()) - .strategy(Strategy.SWARM) - .maxTurns(5) - .handoffs( - OnTextMention.of("reverse", "e2e_java_text"), - OnTextMention.of("compute", "e2e_java_math") - ) - .build(); + .name("e2e_java_swarm_parent") + .model(MODEL) + .instructions( + "You are a coordinator. When asked to reverse text, mention 'reverse' to route to the text agent.") + .agents(mathAgent(), textAgent()) + .strategy(Strategy.SWARM) + .maxTurns(5) + .handoffs(OnTextMention.of("reverse", "e2e_java_text"), OnTextMention.of("compute", "e2e_java_math")) + .build(); AgentResult result = runtime.run(parent, "Please reverse the word hello"); // Accept any terminal status — the key assertion is the sub-workflow assertTrue( - result.getStatus() == AgentStatus.COMPLETED - || result.getStatus() == AgentStatus.FAILED - || result.getStatus() == AgentStatus.TERMINATED, - "Expected a terminal status (COMPLETED/FAILED/TERMINATED). Got: " + result.getStatus()); + result.getStatus() == AgentStatus.COMPLETED + || result.getStatus() == AgentStatus.FAILED + || result.getStatus() == AgentStatus.TERMINATED, + "Expected a terminal status (COMPLETED/FAILED/TERMINATED). Got: " + result.getStatus()); String executionId = result.getExecutionId(); assertNotNull(executionId, "executionId is null"); @@ -423,19 +441,21 @@ void test_swarm_with_text_mention() { List> allExecTasks = (List>) workflow.get("tasks"); assertNotNull(allExecTasks, "workflow has no 'tasks' field"); - boolean textSubWorkflowFound = allExecTasks.stream() - .anyMatch(t -> { - String taskType = (String) t.getOrDefault("taskType", ""); - String ref = (String) t.getOrDefault("referenceTaskName", ""); - return "SUB_WORKFLOW".equals(taskType) && ref.contains("text"); - }); - - assertTrue(textSubWorkflowFound, - "Expected at least one SUB_WORKFLOW task with referenceTaskName containing 'text'. " - + "COUNTERFACTUAL: if OnTextMention 'reverse' trigger doesn't fire, " - + "the text_agent sub-workflow is never created. " - + "Execution tasks found: " + allExecTasks.stream() - .map(t -> t.getOrDefault("taskType", "?") + ":" + t.getOrDefault("referenceTaskName", "?")) - .collect(Collectors.toList())); + boolean textSubWorkflowFound = allExecTasks.stream().anyMatch(t -> { + String taskType = (String) t.getOrDefault("taskType", ""); + String ref = (String) t.getOrDefault("referenceTaskName", ""); + return "SUB_WORKFLOW".equals(taskType) && ref.contains("text"); + }); + + assertTrue( + textSubWorkflowFound, + "Expected at least one SUB_WORKFLOW task with referenceTaskName containing 'text'. " + + "COUNTERFACTUAL: if OnTextMention 'reverse' trigger doesn't fire, " + + "the text_agent sub-workflow is never created. " + + "Execution tasks found: " + + allExecTasks.stream() + .map(t -> t.getOrDefault("taskType", "?") + ":" + + t.getOrDefault("referenceTaskName", "?")) + .collect(Collectors.toList())); } } diff --git a/sdk/java/e2e/SuiteHttpApi404.java b/sdk/java/e2e/SuiteHttpApi404.java index 5d4229761..b06225539 100644 --- a/sdk/java/e2e/SuiteHttpApi404.java +++ b/sdk/java/e2e/SuiteHttpApi404.java @@ -1,25 +1,28 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; -import ai.agentspan.AgentConfig; -import ai.agentspan.exceptions.AgentAPIException; -import ai.agentspan.exceptions.AgentNotFoundException; -import ai.agentspan.exceptions.AgentspanException; -import ai.agentspan.internal.HttpApi; +import org.conductoross.conductor.ai.exceptions.AgentAPIException; +import org.conductoross.conductor.ai.exceptions.AgentNotFoundException; +import org.conductoross.conductor.ai.exceptions.AgentspanException; +import org.conductoross.conductor.ai.internal.AgentClient; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import com.netflix.conductor.client.http.ConductorClient; + +import io.orkes.conductor.client.ApiClient; /** - * Live 404 round-trip — proves HttpApi maps server 404 responses to the + * Live 404 round-trip — proves {@link AgentClient} maps server 404 responses + * (raised by the Conductor client as {@code ConductorClientException}) to the * narrower {@link AgentNotFoundException} subtype (Python-SDK parity). * - *

    Counterfactual: if HttpApi raised the generic {@link AgentAPIException} - * for every 4xx (the pre-refactor behavior), the {@code assertInstanceOf} + *

    Counterfactual: if AgentClient raised the generic {@link AgentAPIException} + * for every 4xx (or leaked Conductor's own exception), the {@code assertInstanceOf} * check below would fail. */ @Tag("e2e") @@ -27,18 +30,19 @@ class SuiteHttpApi404 extends BaseTest { @Test void getStatusOnMissingExecutionIdRaisesAgentNotFoundException() { - HttpApi api = new HttpApi(new AgentConfig(BASE_URL, null, null, 100, 1)); - - AgentAPIException ex = assertThrows( - AgentAPIException.class, - () -> api.getAgentStatus("does-not-exist-" + System.nanoTime()) - ); - - assertInstanceOf(AgentNotFoundException.class, ex, - "404 must surface as AgentNotFoundException, not generic AgentAPIException"); - assertInstanceOf(AgentspanException.class, ex, - "AgentNotFoundException must remain catchable as the SDK base type"); - assertTrue(ex.getStatusCode() == 404, - "Expected statusCode=404, got " + ex.getStatusCode()); + ConductorClient cc = new ApiClient( + (BASE_URL.endsWith("/") ? BASE_URL.substring(0, BASE_URL.length() - 1) : BASE_URL) + "/api"); + AgentClient api = new AgentClient(cc); + + AgentAPIException ex = + assertThrows(AgentAPIException.class, () -> api.getAgentStatus("does-not-exist-" + System.nanoTime())); + + assertInstanceOf( + AgentNotFoundException.class, + ex, + "404 must surface as AgentNotFoundException, not generic AgentAPIException"); + assertInstanceOf( + AgentspanException.class, ex, "AgentNotFoundException must remain catchable as the SDK base type"); + assertTrue(ex.getStatusCode() == 404, "Expected statusCode=404, got " + ex.getStatusCode()); } } diff --git a/sdk/java/examples/VERIFICATION.md b/sdk/java/examples/VERIFICATION.md index f2527326c..4716b30de 100644 --- a/sdk/java/examples/VERIFICATION.md +++ b/sdk/java/examples/VERIFICATION.md @@ -17,7 +17,7 @@ all execute **server-side**. > is **no native OpenAI Agents Java SDK** at the time of this writing — > only the raw `com.openai:openai-java` HTTP client, which has zero agent > abstractions. The OpenAI examples therefore use Agentspan's own -> `OpenAIAgent.builder()` (in `ai.agentspan.frameworks`) — that builder +> `OpenAIAgent.builder()` (in `org.conductoross.conductor.ai.frameworks`) — that builder > IS the Java equivalent of the Python `openai-agents` library, not a > bridge over something native. The same bug-bounty fixes applied to > `AdkBridge` and `LangChain4jAgent` (rich coercion via @@ -27,7 +27,7 @@ all execute **server-side**. ## What "server-side execution" means here For each example we ran the user code unchanged, captured the -execution ID returned by `Agentspan.run(...)`, then queried +execution ID returned by `runtime.run(...)`, then queried `GET /api/workflow/{executionId}?includeTasks=true` to count and classify the tasks the server actually scheduled. The shapes that should appear in those task lists, per pattern: @@ -56,7 +56,7 @@ java -jar build/libs/agentspan-runtime.jar # 2. Run a single example cd sdk/java AGENTSPAN_LLM_MODEL=openai/gpt-4o-mini \ - ./gradlew :examples:run -PmainClass=ai.agentspan.examples.adk.Example02FunctionTools + ./gradlew :examples:run -PmainClass=org.conductoross.conductor.ai.examples.adk.Example02FunctionTools # 3. Inspect the workflow EXEC= diff --git a/sdk/java/examples/build.gradle b/sdk/java/examples/build.gradle index fa9ef8aad..15e589712 100644 --- a/sdk/java/examples/build.gradle +++ b/sdk/java/examples/build.gradle @@ -3,7 +3,7 @@ plugins { id 'application' } -group = 'ai.agentspan' +group = 'org.conductoross.conductor' version = '0.1.0' java { @@ -42,9 +42,9 @@ dependencies { compileJava.options.compilerArgs << '-parameters' // Allow running individual examples via: -// ./gradlew :examples:run -PmainClass=ai.agentspan.examples.openai.Example01BasicAgent -// ./gradlew :examples:run -PmainClass=ai.agentspan.examples.adk.Example00HelloWorld -// ./gradlew :examples:run -PmainClass=ai.agentspan.examples.langchain.Example09MathCalculator +// ./gradlew :examples:run -PmainClass=org.conductoross.conductor.ai.examples.openai.Example01BasicAgent +// ./gradlew :examples:run -PmainClass=org.conductoross.conductor.ai.examples.adk.Example00HelloWorld +// ./gradlew :examples:run -PmainClass=org.conductoross.conductor.ai.examples.langchain.Example09MathCalculator application { - mainClass = project.findProperty('mainClass') ?: 'ai.agentspan.examples.Example01BasicAgent' + mainClass = project.findProperty('mainClass') ?: 'org.conductoross.conductor.ai.examples.Example01BasicAgent' } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example01BasicAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example01BasicAgent.java similarity index 65% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example01BasicAgent.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example01BasicAgent.java index e48be0842..bf0825905 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example01BasicAgent.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example01BasicAgent.java @@ -1,11 +1,11 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 01 — Basic Agent @@ -20,15 +20,16 @@ */ public class Example01BasicAgent { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); Agent agent = Agent.builder() .name("basic_assistant") .model(Settings.LLM_MODEL) .instructions("You are a helpful assistant.") .build(); - AgentResult result = Agentspan.run(agent, "What is the capital of France?"); + AgentResult result = runtime.run(agent, "What is the capital of France?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example02Tools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example02Tools.java similarity index 71% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example02Tools.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example02Tools.java index 6f1a127e0..88c6f0c32 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example02Tools.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example02Tools.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -35,6 +35,7 @@ public Map getStockPrice(String symbol) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new AgentTools()); Agent agent = Agent.builder() @@ -44,9 +45,9 @@ public static void main(String[] args) { .instructions("You are a helpful assistant. Use tools to answer questions.") .build(); - AgentResult result = Agentspan.run(agent, "What's the weather like in San Francisco?"); + AgentResult result = runtime.run(agent, "What's the weather like in San Francisco?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example02aSimpleTools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example02aSimpleTools.java similarity index 73% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example02aSimpleTools.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example02aSimpleTools.java index 09c702213..ae73b1b07 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example02aSimpleTools.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example02aSimpleTools.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -37,6 +37,7 @@ public Map getStockPrice(String symbol) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new AssistantTools()); Agent agent = Agent.builder() @@ -47,9 +48,9 @@ public static void main(String[] args) { .build(); // The LLM will call get_weather (not get_stock_price) - AgentResult result = Agentspan.run(agent, "What's the weather like in San Francisco?"); + AgentResult result = runtime.run(agent, "What's the weather like in San Francisco?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example02bMultiStepTools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example02bMultiStepTools.java similarity index 88% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example02bMultiStepTools.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example02bMultiStepTools.java index c3e5f9f2a..d02ada251 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example02bMultiStepTools.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example02bMultiStepTools.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -74,6 +74,7 @@ public Map sendSummaryEmail(String to, String subject, String bo } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new AccountTools()); Agent agent = Agent.builder() @@ -86,11 +87,11 @@ public static void main(String[] args) { + "Use the tools step by step.") .build(); - AgentResult result = Agentspan.run(agent, + AgentResult result = runtime.run(agent, "How much has alice@example.com spent recently? " + "Get her last 3 transactions and give me the total."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example02cTypedToolArgs.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example02cTypedToolArgs.java similarity index 82% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example02cTypedToolArgs.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example02cTypedToolArgs.java index 1f85fa6fd..9b90de240 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example02cTypedToolArgs.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example02cTypedToolArgs.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.time.Duration; import java.time.Instant; @@ -51,6 +51,7 @@ public Map recordEvent(Instant when) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new CalendarTools()); Agent agent = Agent.builder() @@ -62,11 +63,11 @@ public static void main(String[] args) { + "and record events. Pass dates and times exactly as the user gives them.") .build(); - AgentResult result = Agentspan.run(agent, + AgentResult result = runtime.run(agent, "Schedule a one-hour meeting starting May 12th, 2026 at 2 PM, " + "then record an event at 2026-05-12T13:45:00Z."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example03StructuredOutput.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example03StructuredOutput.java similarity index 80% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example03StructuredOutput.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example03StructuredOutput.java index c5b9ddcf0..daf57b97c 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example03StructuredOutput.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example03StructuredOutput.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -43,6 +43,7 @@ public Map getWeather(String city) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); WeatherTools weatherTools = new WeatherTools(); List tools = ToolRegistry.fromInstance(weatherTools); @@ -54,7 +55,7 @@ public static void main(String[] args) { .outputType(WeatherReport.class) .build(); - AgentResult result = Agentspan.run(agent, "What's the weather in NYC?"); + AgentResult result = runtime.run(agent, "What's the weather in NYC?"); result.printResult(); // Get the typed output @@ -69,6 +70,6 @@ public static void main(String[] args) { } } - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example04HttpAndMcpTools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example04HttpAndMcpTools.java similarity index 85% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example04HttpAndMcpTools.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example04HttpAndMcpTools.java index ad017f50d..bdc1c4b5b 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example04HttpAndMcpTools.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example04HttpAndMcpTools.java @@ -1,16 +1,16 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; -import ai.agentspan.tools.HttpTool; -import ai.agentspan.tools.McpTool; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.tools.HttpTool; +import org.conductoross.conductor.ai.tools.McpTool; import java.util.List; import java.util.Map; @@ -49,6 +49,7 @@ public String formatReport(Map data) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Local worker tool List localTools = ToolRegistry.fromInstance(new ReportTools()); @@ -101,10 +102,10 @@ public static void main(String[] args) { .maxTokens(102040) .build(); - AgentResult result = Agentspan.run(agent, + AgentResult result = runtime.run(agent, "Get the weather in London and format it as a report."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example05Handoffs.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example05Handoffs.java similarity index 82% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example05Handoffs.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example05Handoffs.java index 7a7b877e1..ce0e0399f 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example05Handoffs.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example05Handoffs.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -44,6 +44,7 @@ public Map getPricing(String product) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List billingTools = ToolRegistry.fromInstance(new BillingTools()); List technicalTools = ToolRegistry.fromInstance(new TechnicalTools()); List salesTools = ToolRegistry.fromInstance(new SalesTools()); @@ -79,9 +80,9 @@ public static void main(String[] args) { .strategy(Strategy.HANDOFF) .build(); - AgentResult result = Agentspan.run(support, "What's the balance on account ACC-123?"); + AgentResult result = runtime.run(support, "What's the balance on account ACC-123?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example06SequentialPipeline.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example06SequentialPipeline.java similarity index 85% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example06SequentialPipeline.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example06SequentialPipeline.java index ff74d8a60..ce562d351 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example06SequentialPipeline.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example06SequentialPipeline.java @@ -1,11 +1,11 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 06 — Sequential Pipeline @@ -16,6 +16,7 @@ public class Example06SequentialPipeline { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Step 1: Researcher gathers information Agent researcher = Agent.builder() .name("researcher") @@ -51,10 +52,10 @@ public static void main(String[] args) { System.out.println("Pipeline: " + contentPipeline.getName()); System.out.println("Sub-agents: " + contentPipeline.getAgents().size()); - AgentResult result = Agentspan.run(contentPipeline, + AgentResult result = runtime.run(contentPipeline, "Write an article about the future of renewable energy"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example07ParallelAgents.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example07ParallelAgents.java similarity index 84% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example07ParallelAgents.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example07ParallelAgents.java index eec83cf56..f51d1796c 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example07ParallelAgents.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example07ParallelAgents.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 07 — Parallel Agents @@ -17,6 +17,7 @@ public class Example07ParallelAgents { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Analysts running in parallel Agent technicalAnalyst = Agent.builder() .name("technical_analyst") @@ -53,10 +54,10 @@ public static void main(String[] args) { .strategy(Strategy.PARALLEL) .build(); - AgentResult result = Agentspan.run(analysisTeam, + AgentResult result = runtime.run(analysisTeam, "Analyze the adoption of AI in healthcare for patient diagnosis"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example08RouterAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example08RouterAgent.java similarity index 85% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example08RouterAgent.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example08RouterAgent.java index 2c92f7a24..ce5fab714 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example08RouterAgent.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example08RouterAgent.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 08 — Router Agent @@ -17,6 +17,7 @@ public class Example08RouterAgent { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Specialist agents Agent pythonExpert = Agent.builder() .name("python_expert") @@ -66,15 +67,15 @@ public static void main(String[] args) { // Test with different questions System.out.println("=== Python Question ==="); - AgentResult pythonResult = Agentspan.run(codingAssistant, + AgentResult pythonResult = runtime.run(codingAssistant, "How do I use list comprehensions in Python?"); pythonResult.printResult(); System.out.println("=== SQL Question ==="); - AgentResult sqlResult = Agentspan.run(codingAssistant, + AgentResult sqlResult = runtime.run(codingAssistant, "How do I write a SQL query to find the top 10 customers by revenue?"); sqlResult.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example09HumanInTheLoop.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example09HumanInTheLoop.java similarity index 87% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example09HumanInTheLoop.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example09HumanInTheLoop.java index 1ef0fb07d..7edae9a40 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example09HumanInTheLoop.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example09HumanInTheLoop.java @@ -1,16 +1,16 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.EventType; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentEvent; -import ai.agentspan.model.AgentStream; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.EventType; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentEvent; +import org.conductoross.conductor.ai.model.AgentStream; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example09bHandoffHumanInTheLoop.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example09bHandoffHumanInTheLoop.java similarity index 88% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example09bHandoffHumanInTheLoop.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example09bHandoffHumanInTheLoop.java index 041e290e3..5c4442f83 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example09bHandoffHumanInTheLoop.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example09bHandoffHumanInTheLoop.java @@ -1,17 +1,17 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.EventType; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentEvent; -import ai.agentspan.model.AgentStream; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.EventType; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentEvent; +import org.conductoross.conductor.ai.model.AgentStream; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example108PlanExecuteRefs.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example108PlanExecuteRefs.java similarity index 91% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example108PlanExecuteRefs.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example108PlanExecuteRefs.java index 41706b92c..4c911cbdd 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example108PlanExecuteRefs.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example108PlanExecuteRefs.java @@ -1,18 +1,18 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; - -import ai.agentspan.Agent; -import ai.agentspan.AgentConfig; -import ai.agentspan.AgentRuntime; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; -import ai.agentspan.plans.Op; -import ai.agentspan.plans.Plan; -import ai.agentspan.plans.Ref; -import ai.agentspan.plans.Step; +package org.conductoross.conductor.ai.examples; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.plans.Op; +import org.conductoross.conductor.ai.plans.Plan; +import org.conductoross.conductor.ai.plans.Ref; +import org.conductoross.conductor.ai.plans.Step; import java.net.URI; import java.net.http.HttpClient; @@ -41,7 +41,7 @@ * to format a final summary. The plan is fully deterministic — no planner * LLM required — because we pass it directly to {@code runtime.run}. * - *

    Run: {@code ./gradlew :examples:run -PmainClass=ai.agentspan.examples.Example108PlanExecuteRefs} + *

    Run: {@code ./gradlew :examples:run -PmainClass=org.conductoross.conductor.ai.examples.Example108PlanExecuteRefs} */ public class Example108PlanExecuteRefs { @@ -157,7 +157,7 @@ public static void main(String[] args) throws Exception { .build(); try (AgentRuntime runtime = new AgentRuntime( - new AgentConfig(BASE_URL + "/api", null, null, 100, 1))) { + AgentRuntime.client(BASE_URL), new AgentConfig(100, 1))) { AgentResult result = runtime.run(harness, "demo", plan); System.out.println("status=" + result.getStatus() + " executionId=" + result.getExecutionId()); diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example10Guardrails.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example10Guardrails.java similarity index 79% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example10Guardrails.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example10Guardrails.java index 1c565476f..a2a21563a 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example10Guardrails.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example10Guardrails.java @@ -1,17 +1,17 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.GuardrailDef; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.GuardrailResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.GuardrailDef; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.GuardrailResult; import java.util.List; import java.util.Map; @@ -71,8 +71,9 @@ public GuardrailResult noPii(String content) { } public static void main(String[] args) { - List tools = ToolRegistry.fromInstance(new CustomerTools()); - List guardrails = + AgentRuntime runtime = new AgentRuntime(); + List tools = ToolRegistry.fromInstance(new CustomerTools()); + List guardrails = ToolRegistry.guardrailsFromInstance(new PiiGuardrails()); Agent agent = Agent.builder() @@ -86,7 +87,7 @@ public static void main(String[] args) { .guardrails(guardrails) .build(); - AgentResult result = Agentspan.run(agent, + AgentResult result = runtime.run(agent, "I need a full summary: What's the status of order ORD-42, " + "and what's the profile for customer CUST-7?"); result.printResult(); @@ -98,6 +99,6 @@ public static void main(String[] args) { System.out.println("[OK] PII was redacted from the final output."); } - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example115PlannerContext.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example115PlannerContext.java similarity index 96% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example115PlannerContext.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example115PlannerContext.java index f91a507e5..90b3edd6f 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example115PlannerContext.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example115PlannerContext.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; -import ai.agentspan.plans.Context; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.plans.Context; import java.net.URI; import java.net.http.HttpClient; @@ -45,7 +45,7 @@ *

    Mirrors sdk/python/examples/115_plan_execute_planner_context.py and * sdk/typescript/examples/115-plan-execute-planner-context.ts. * - *

    Run: {@code ./gradlew :examples:run -PmainClass=ai.agentspan.examples.Example115PlannerContext} + *

    Run: {@code ./gradlew :examples:run -PmainClass=org.conductoross.conductor.ai.examples.Example115PlannerContext} */ public class Example115PlannerContext { diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example11Streaming.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example11Streaming.java similarity index 90% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example11Streaming.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example11Streaming.java index 2ae2fdbf7..49cc946bb 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example11Streaming.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example11Streaming.java @@ -1,17 +1,17 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; - -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.EventType; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentEvent; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.AgentStream; -import ai.agentspan.model.ToolDef; +package org.conductoross.conductor.ai.examples; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.EventType; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentEvent; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.AgentStream; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example12LongRunning.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example12LongRunning.java similarity index 86% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example12LongRunning.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example12LongRunning.java index c72301801..fa41af740 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example12LongRunning.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example12LongRunning.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.model.AgentHandle; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentHandle; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 12 — Long-Running Agent (fire-and-forget with polling) diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example13HierarchicalAgents.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example13HierarchicalAgents.java similarity index 90% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example13HierarchicalAgents.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example13HierarchicalAgents.java index c3982b586..ca72cf4ba 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example13HierarchicalAgents.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example13HierarchicalAgents.java @@ -1,13 +1,13 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.handoff.OnTextMention; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.handoff.OnTextMention; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 13 — Hierarchical Agents (nested agent teams) @@ -28,6 +28,7 @@ public class Example13HierarchicalAgents { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Level 3: Individual specialists ───────────────────────────── Agent backendDev = Agent.builder() @@ -104,11 +105,11 @@ public static void main(String[] args) { .build(); System.out.println("--- Technical question (CEO -> Engineering -> Backend) ---"); - AgentResult result = Agentspan.run(ceo, + AgentResult result = runtime.run(ceo, "Design a REST API for a user management system with authentication " + "and then come up with a marketing campaign for the system"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example14ExistingWorkers.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example14ExistingWorkers.java similarity index 87% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example14ExistingWorkers.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example14ExistingWorkers.java index 84c53bd8d..4f2629917 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example14ExistingWorkers.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example14ExistingWorkers.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -74,6 +74,7 @@ public Map createSupportTicket(String customerId, String issue, } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new CustomerSupportTools()); Agent agent = Agent.builder() @@ -85,10 +86,10 @@ public static void main(String[] args) { + "customer information, check order history, and create support tickets.") .build(); - AgentResult result = Agentspan.run(agent, + AgentResult result = runtime.run(agent, "Customer C001 is asking about their recent orders. Look them up and summarize."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example15AgentDiscussion.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example15AgentDiscussion.java similarity index 89% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example15AgentDiscussion.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example15AgentDiscussion.java index dd3dceada..b5bd37786 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example15AgentDiscussion.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example15AgentDiscussion.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 15 — Agent Discussion (round-robin debate) @@ -24,6 +24,7 @@ public class Example15AgentDiscussion { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Discussion participants ──────────────────────────────────────── Agent optimist = Agent.builder() @@ -77,10 +78,10 @@ public static void main(String[] args) { .strategy(Strategy.SEQUENTIAL) .build(); - AgentResult result = Agentspan.run(pipeline, + AgentResult result = runtime.run(pipeline, "Should AI agents be allowed to autonomously make financial decisions for individuals?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example16CredentialsTool.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example16CredentialsTool.java similarity index 86% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example16CredentialsTool.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example16CredentialsTool.java index e40eb82d7..8576a6b69 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example16CredentialsTool.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example16CredentialsTool.java @@ -1,16 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.Credentials; -import ai.agentspan.annotations.Tool; -import ai.agentspan.exceptions.CredentialNotFoundException; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolContext; +import org.conductoross.conductor.ai.model.ToolDef; import java.io.IOException; import java.net.URI; @@ -27,12 +26,12 @@ *

    Demonstrates the {@code credentials} field on {@code @Tool}. Declared * credential names are resolved by the server before each tool call. The * worker fetches the value via {@code POST /api/workers/secrets} using the - * execution token, then makes it available to the tool body via - * {@link ai.agentspan.Credentials#get(String)}. + * execution token, then makes it available to the tool body via the per-call + * {@link org.conductoross.conductor.ai.model.ToolContext#getCredential(String)}. * *

    Java is tier-1-only — {@code System.getenv()} is immutable at JVM * runtime, so unlike Python/.NET/TypeScript there is no env-injection mode. - * Tools MUST read declared credentials via {@code Credentials.get(name)}; reading + * Tools MUST read declared credentials via {@code ctx.getCredential(name)}; reading * via {@code System.getenv} would only see whatever the JVM inherited from * the shell at startup. See {@code docs/design/secret-injection-contract.md} §6. * @@ -58,13 +57,13 @@ static class GithubTools { description = "List public repositories for a GitHub username (most recently updated)", credentials = {"GITHUB_TOKEN"} ) - public Map listGithubRepos(String username, int limit) { + public Map listGithubRepos(String username, int limit, ToolContext ctx) { try { int n = limit > 0 ? Math.min(limit, 10) : 5; // GITHUB_TOKEN was resolved by the worker before this handler ran - // (via POST /api/workers/secrets) and is available through the - // Credentials thread-local accessor — no env-var mutation involved. - String token = Credentials.getOrNull("GITHUB_TOKEN"); + // (via POST /api/workers/secrets) and is available on the per-call + // ToolContext — no env-var mutation involved. + String token = ctx.getCredentialOrNull("GITHUB_TOKEN"); HttpRequest.Builder reqBuilder = HttpRequest.newBuilder() .uri(URI.create("https://api.github.com/users/" + username + "/repos?per_page=" + n + "&sort=updated")) @@ -96,9 +95,9 @@ public Map listGithubRepos(String username, int limit) { description = "Get profile information for a GitHub user", credentials = {"GITHUB_TOKEN"} ) - public Map getGithubUser(String username) { + public Map getGithubUser(String username, ToolContext ctx) { try { - String token = Credentials.getOrNull("GITHUB_TOKEN"); + String token = ctx.getCredentialOrNull("GITHUB_TOKEN"); HttpRequest.Builder reqBuilder = HttpRequest.newBuilder() .uri(URI.create("https://api.github.com/users/" + username)) .timeout(Duration.ofSeconds(10)) @@ -147,6 +146,7 @@ private static int countOccurrences(String text, String pattern) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new GithubTools()); Agent agent = Agent.builder() @@ -158,10 +158,10 @@ public static void main(String[] args) { + "their repositories. Use the available tools to answer questions.") .build(); - AgentResult result = Agentspan.run(agent, + AgentResult result = runtime.run(agent, "Look up the GitHub user 'torvalds' and show their most recent 3 repositories."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example16RandomStrategy.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example16RandomStrategy.java similarity index 83% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example16RandomStrategy.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example16RandomStrategy.java index 90d2e04ad..0f8d71017 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example16RandomStrategy.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example16RandomStrategy.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 16 — Random Strategy (random agent selection each turn) @@ -21,6 +21,7 @@ public class Example16RandomStrategy { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); Agent creative = Agent.builder() .name("creative") .model(Settings.LLM_MODEL) @@ -54,10 +55,10 @@ public static void main(String[] args) { .maxTurns(6) .build(); - AgentResult result = Agentspan.run(brainstorm, + AgentResult result = runtime.run(brainstorm, "How should we approach building an AI-powered customer service platform?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example17SwarmOrchestration.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example17SwarmOrchestration.java similarity index 85% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example17SwarmOrchestration.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example17SwarmOrchestration.java index ffc56d83b..f44e5bb58 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example17SwarmOrchestration.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example17SwarmOrchestration.java @@ -1,13 +1,13 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.handoff.OnTextMention; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.handoff.OnTextMention; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 17 — Swarm Orchestration (LLM-driven agent transitions) @@ -25,6 +25,7 @@ public class Example17SwarmOrchestration { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Specialist agents ──────────────────────────────────────────── Agent refundAgent = Agent.builder() @@ -66,15 +67,15 @@ public static void main(String[] args) { // ── Run test scenarios ─────────────────────────────────────────── System.out.println("=== Refund Scenario ==="); - AgentResult refundResult = Agentspan.run(support, + AgentResult refundResult = runtime.run(support, "I bought a product last week and it arrived damaged. I want my money back."); refundResult.printResult(); System.out.println("\n=== Technical Issue Scenario ==="); - AgentResult techResult = Agentspan.run(support, + AgentResult techResult = runtime.run(support, "My app keeps crashing whenever I try to upload a file larger than 10MB."); techResult.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example18ManualSelection.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example18ManualSelection.java similarity index 88% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example18ManualSelection.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example18ManualSelection.java index ab2ad401d..614271303 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example18ManualSelection.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example18ManualSelection.java @@ -1,13 +1,13 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentHandle; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentHandle; +import org.conductoross.conductor.ai.model.AgentResult; import java.util.List; import java.util.Map; @@ -33,6 +33,7 @@ public class Example18ManualSelection { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); Agent writer = Agent.builder() .name("writer") .model(Settings.LLM_MODEL) @@ -72,7 +73,7 @@ public static void main(String[] args) { "Draft a short paragraph about the discovery of penicillin, " + "then have it reviewed for accuracy and style."; - AgentHandle handle = Agentspan.start(editorialTeam, prompt); + AgentHandle handle = runtime.start(editorialTeam, prompt); System.out.println("Execution ID: " + handle.getExecutionId()); // Drive the 3 manual turns. Each turn the MANUAL strategy creates a @@ -96,6 +97,6 @@ public static void main(String[] args) { AgentResult result = handle.waitForResult(); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example19ComposableTermination.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example19ComposableTermination.java similarity index 80% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example19ComposableTermination.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example19ComposableTermination.java index 1a6df7c90..1edbfa00b 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example19ComposableTermination.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example19ComposableTermination.java @@ -1,16 +1,16 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.termination.MaxMessageTermination; -import ai.agentspan.termination.TextMentionTermination; -import ai.agentspan.termination.TokenUsageTermination; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.termination.MaxMessageTermination; +import org.conductoross.conductor.ai.termination.TextMentionTermination; +import org.conductoross.conductor.ai.termination.TokenUsageTermination; import java.util.List; @@ -37,7 +37,8 @@ public String search(String query) { } public static void main(String[] args) { - List searchTools = + AgentRuntime runtime = new AgentRuntime(); + List searchTools = ToolRegistry.fromInstance(new SearchTools()); // ── Example 1: Simple text mention ──────────────────────────────── @@ -51,7 +52,7 @@ public static void main(String[] args) { .build(); System.out.println("=== Example 1: TextMentionTermination (stop on DONE) ==="); - AgentResult r1 = Agentspan.run(researcher, "What are AI agents?"); + AgentResult r1 = runtime.run(researcher, "What are AI agents?"); r1.printResult(); // ── Example 2: OR — stop on text OR after 5 messages ───────────── @@ -66,7 +67,7 @@ public static void main(String[] args) { .build(); System.out.println("\n=== Example 2: OR termination (GOODBYE or 5 messages) ==="); - AgentResult r2 = Agentspan.run(chatbot, "Tell me a short fun fact about space."); + AgentResult r2 = runtime.run(chatbot, "Tell me a short fun fact about space."); r2.printResult(); // ── Example 3: AND — stop when BOTH conditions met ──────────────── @@ -85,7 +86,7 @@ public static void main(String[] args) { .build(); System.out.println("\n=== Example 3: AND termination (FINAL ANSWER + 3 messages) ==="); - AgentResult r3 = Agentspan.run(deliberator, "What are the main types of AI agents?"); + AgentResult r3 = runtime.run(deliberator, "What are the main types of AI agents?"); r3.printResult(); // ── Example 4: Complex composition ──────────────────────────────── @@ -104,10 +105,10 @@ public static void main(String[] args) { .build(); System.out.println("\n=== Example 4: Complex composition (TERMINATE | (DONE & 5msg) | tokens) ==="); - AgentResult r4 = Agentspan.run(complexAgent, + AgentResult r4 = runtime.run(complexAgent, "Summarize the key benefits of multi-agent AI systems."); r4.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example20ConstrainedTransitions.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example20ConstrainedTransitions.java similarity index 86% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example20ConstrainedTransitions.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example20ConstrainedTransitions.java index ec74105d0..4ba6a6934 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example20ConstrainedTransitions.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example20ConstrainedTransitions.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; import java.util.List; import java.util.Map; @@ -28,6 +28,7 @@ public class Example20ConstrainedTransitions { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); Agent developer = Agent.builder() .name("developer") .model(Settings.LLM_MODEL) @@ -66,10 +67,10 @@ public static void main(String[] args) { )) .build(); - AgentResult result = Agentspan.run(codeReview, + AgentResult result = runtime.run(codeReview, "Write a Python function to validate email addresses using regex."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example21RegexGuardrails.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example21RegexGuardrails.java similarity index 80% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example21RegexGuardrails.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example21RegexGuardrails.java index eed2e4ca0..444603301 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example21RegexGuardrails.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example21RegexGuardrails.java @@ -1,17 +1,17 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -44,6 +44,7 @@ public Map getUserProfile(String userId) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new ProfileTools()); // ── Block email addresses ───────────────────────────────────────── @@ -82,9 +83,9 @@ public static void main(String[] args) { .guardrails(List.of(noEmails, noSsn)) .build(); - AgentResult result = Agentspan.run(agent, "Tell me everything about user U-001."); + AgentResult result = runtime.run(agent, "Tell me everything about user U-001."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example22LlmGuardrails.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example22LlmGuardrails.java similarity index 83% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example22LlmGuardrails.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example22LlmGuardrails.java index ce82563fa..4f48a6fa8 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example22LlmGuardrails.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example22LlmGuardrails.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.GuardrailDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.GuardrailDef; import java.util.List; import java.util.Map; @@ -26,6 +26,7 @@ public class Example22LlmGuardrails { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── LLM-based tone guardrail ─────────────────────────────────────── // Ensures customer communications are professional and positive. @@ -61,10 +62,10 @@ public static void main(String[] args) { .guardrails(List.of(toneGuard)) .build(); - AgentResult result = Agentspan.run(agent, + AgentResult result = runtime.run(agent, "A customer is frustrated that their order arrived late. Write a response."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example23TokenTracking.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example23TokenTracking.java similarity index 83% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example23TokenTracking.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example23TokenTracking.java index 56cf2ab11..2cf608a5d 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example23TokenTracking.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example23TokenTracking.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; -import ai.agentspan.model.TokenUsage; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.model.TokenUsage; import java.util.List; import java.util.Map; @@ -44,6 +44,7 @@ public String calculate(String expression) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new MathTools()); Agent agent = Agent.builder() @@ -55,7 +56,7 @@ public static void main(String[] args) { + "tool for computations. Explain each step clearly.") .build(); - AgentResult result = Agentspan.run(agent, + AgentResult result = runtime.run(agent, "Calculate the compound interest on $10,000 at 5% annual rate " + "compounded monthly for 3 years."); result.printResult(); @@ -78,6 +79,6 @@ public static void main(String[] args) { System.out.println("Token usage not available from server."); } - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example29AgentIntroductions.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example29AgentIntroductions.java similarity index 89% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example29AgentIntroductions.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example29AgentIntroductions.java index dccb9d382..fdecb6c0b 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example29AgentIntroductions.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example29AgentIntroductions.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 29 — Agent Introductions @@ -21,6 +21,7 @@ public class Example29AgentIntroductions { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Agents with introductions ────────────────────────────────────── Agent architect = Agent.builder() @@ -70,11 +71,11 @@ public static void main(String[] args) { .maxTurns(6) .build(); - AgentResult result = Agentspan.run(designReview, + AgentResult result = runtime.run(designReview, "Review the design for a new user authentication system that uses " + "passkeys (WebAuthn) instead of passwords."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example31ToolInputGuardrail.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example31ToolInputGuardrail.java similarity index 78% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example31ToolInputGuardrail.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example31ToolInputGuardrail.java index 94e3a5ad4..f7c2fb527 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example31ToolInputGuardrail.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example31ToolInputGuardrail.java @@ -1,18 +1,18 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.GuardrailResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.GuardrailResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.regex.Pattern; @@ -40,6 +40,7 @@ public String runQuery(String query) { ); public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Register worker AND get initial ToolDef List rawTools = ToolRegistry.fromInstance(new DbTools()); ToolDef rawTool = rawTools.get(0); @@ -79,14 +80,14 @@ public static void main(String[] args) { .build(); System.out.println("=== Safe Query ==="); - AgentResult result1 = Agentspan.run(agent, "Find all users older than 25."); + AgentResult result1 = runtime.run(agent, "Find all users older than 25."); result1.printResult(); System.out.println("\n=== Dangerous Query (should be blocked) ==="); - AgentResult result2 = Agentspan.run(agent, + AgentResult result2 = runtime.run(agent, "Run this exact query: SELECT * FROM users; DROP TABLE users; --"); result2.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example32HumanGuardrail.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example32HumanGuardrail.java similarity index 83% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example32HumanGuardrail.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example32HumanGuardrail.java index 45c83e268..28dc26473 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example32HumanGuardrail.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example32HumanGuardrail.java @@ -1,19 +1,19 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentHandle; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.GuardrailResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentHandle; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.GuardrailResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -48,6 +48,7 @@ public Map getMarketData(String ticker) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List marketTools = ToolRegistry.fromInstance(new MarketTools()); // Guardrail: flag regulated financial language — pause for human review on fail @@ -88,7 +89,7 @@ public static void main(String[] args) { .build(); // Start async — the compliance guardrail may pause the workflow - AgentHandle handle = Agentspan.start(financeAgent, + AgentHandle handle = runtime.start(financeAgent, "What is the current price of AAPL and is it a good risk-free investment?"); System.out.println("Execution ID: " + handle.getExecutionId()); @@ -106,6 +107,6 @@ public static void main(String[] args) { AgentResult result = handle.waitForResult(); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example33ExternalWorkers.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example33ExternalWorkers.java similarity index 87% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example33ExternalWorkers.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example33ExternalWorkers.java index a9f0e14a3..f4ef66890 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example33ExternalWorkers.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example33ExternalWorkers.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.LinkedHashMap; import java.util.List; @@ -67,6 +67,7 @@ public Map processOrder(String orderId, String action) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new SupportTools()); Agent supportAgent = Agent.builder() @@ -82,11 +83,11 @@ public static void main(String[] args) { System.out.println("=== Mixed Local + External Worker Tools ==="); System.out.println("(In production, get_customer and process_order run in separate services)\n"); - AgentResult result = Agentspan.run(supportAgent, + AgentResult result = runtime.run(supportAgent, "Customer C-1234 wants to cancel order ORD-5678. " + "Look up the customer, process the cancellation, and give me a formatted summary."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example33SingleTurnTool.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example33SingleTurnTool.java similarity index 72% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example33SingleTurnTool.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example33SingleTurnTool.java index 7f2de9d49..599bdd4fd 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example33SingleTurnTool.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example33SingleTurnTool.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -36,6 +36,7 @@ public Map getWeather(String city) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new WeatherTools()); Agent agent = Agent.builder() @@ -46,9 +47,9 @@ public static void main(String[] args) { .maxTurns(2) .build(); - AgentResult result = Agentspan.run(agent, "What's the weather in San Francisco?"); + AgentResult result = runtime.run(agent, "What's the weather in San Francisco?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example34PromptTemplates.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example34PromptTemplates.java similarity index 77% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example34PromptTemplates.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example34PromptTemplates.java index 72ae1869d..8ab5e5f7d 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example34PromptTemplates.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example34PromptTemplates.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.PromptTemplate; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.PromptTemplate; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -46,6 +46,7 @@ public Map lookupCustomer(String email) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new OrderTools()); // Agent using a server-side prompt template with variable substitution @@ -59,9 +60,9 @@ public static void main(String[] args) { .tools(tools) .build(); - AgentResult result = Agentspan.run(orderAgent, "Can you check order #12345?"); + AgentResult result = runtime.run(orderAgent, "Can you check order #12345?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example35StandaloneGuardrails.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example35StandaloneGuardrails.java similarity index 97% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example35StandaloneGuardrails.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example35StandaloneGuardrails.java index 7e6c4396e..78a1973b6 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example35StandaloneGuardrails.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example35StandaloneGuardrails.java @@ -1,9 +1,9 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.model.GuardrailResult; +import org.conductoross.conductor.ai.model.GuardrailResult; import java.util.function.Function; import java.util.regex.Pattern; @@ -20,7 +20,7 @@ *

  • Reusing the same validation functions in multiple agents
  • * * - *

    This example runs entirely without a server — no {@code Agentspan.run()} call. + *

    This example runs entirely without a server — no {@code runtime.run()} call. */ public class Example35StandaloneGuardrails { @@ -98,7 +98,6 @@ private static void validate( @SuppressWarnings("unchecked") public static void main(String[] args) { - System.out.println("=== Standalone Guardrail Validation (no server required) ==="); // Test 1: Clean text — all checks should pass diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example36SimpleAgentGuardrails.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example36SimpleAgentGuardrails.java similarity index 83% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example36SimpleAgentGuardrails.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example36SimpleAgentGuardrails.java index 852d85864..9ac72d977 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example36SimpleAgentGuardrails.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example36SimpleAgentGuardrails.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.GuardrailResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.GuardrailResult; import java.util.List; import java.util.Map; @@ -30,6 +30,7 @@ public class Example36SimpleAgentGuardrails { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Regex guardrail: block bullet-point lists (server-side InlineTask) ─ GuardrailDef noBulletLists = GuardrailDef.builder() @@ -81,9 +82,9 @@ public static void main(String[] args) { .guardrails(List.of(noBulletLists, minLength)) .build(); - AgentResult result = Agentspan.run(agent, "Explain why the sky is blue."); + AgentResult result = runtime.run(agent, "Explain why the sky is blue."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example37FixGuardrail.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example37FixGuardrail.java similarity index 80% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example37FixGuardrail.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example37FixGuardrail.java index 2ddd6a0a2..44bc16ac9 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example37FixGuardrail.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example37FixGuardrail.java @@ -1,17 +1,17 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.GuardrailResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.GuardrailResult; import java.util.List; import java.util.Map; @@ -60,6 +60,7 @@ public Map getContactInfo(String name) { Pattern.compile("(?:\\+?1[-.\\s]?)?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}"); public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Fix guardrail: redact phone numbers ──────────────────────────── // Instead of asking the LLM to retry, auto-redact and return the fix. @@ -88,13 +89,13 @@ public static void main(String[] args) { .build(); System.out.println("=== Scenario 1: Contact with phone number (guardrail triggers) ==="); - AgentResult r1 = Agentspan.run(agent, "What's Alice Johnson's contact information?"); + AgentResult r1 = runtime.run(agent, "What's Alice Johnson's contact information?"); r1.printResult(); System.out.println("\n=== Scenario 2: Department only (guardrail does not trigger) ==="); - AgentResult r2 = Agentspan.run(agent, "What department does Alice work in? Just the department name."); + AgentResult r2 = runtime.run(agent, "What department does Alice work in? Just the department name."); r2.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example38TechTrends.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example38TechTrends.java similarity index 95% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example38TechTrends.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example38TechTrends.java index 5ea6a624e..87cc1caab 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example38TechTrends.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example38TechTrends.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.io.IOException; import java.net.URI; @@ -173,6 +173,7 @@ private static ToolDef pdfTool() { @SuppressWarnings("unchecked") public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List rawResearcherTools = ToolRegistry.fromInstance(new ResearcherTools()); List rawAnalystTools = ToolRegistry.fromInstance(new AnalystTools()); @@ -246,10 +247,10 @@ public static void main(String[] args) { .strategy(Strategy.SEQUENTIAL) .build(); - AgentResult result = Agentspan.run(pipeline, + AgentResult result = runtime.run(pipeline, "Compare Python and Rust: which has stronger developer mindshare?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example41SequentialPipelineTools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example41SequentialPipelineTools.java similarity index 92% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example41SequentialPipelineTools.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example41SequentialPipelineTools.java index 31895848a..a673a65d5 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example41SequentialPipelineTools.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example41SequentialPipelineTools.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -90,6 +90,7 @@ public Map assembleProduction(String title, int totalScenes, Str @SuppressWarnings("unchecked") public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List conceptTools = ToolRegistry.fromInstance(new ConceptTools()); List rawScriptTools = ToolRegistry.fromInstance(new ScriptTools()); // Python's write_scene has dialogue with a default "" — only first 3 params required @@ -165,11 +166,11 @@ public static void main(String[] args) { .strategy(Strategy.SEQUENTIAL) .build(); - AgentResult result = Agentspan.run(pipeline, + AgentResult result = runtime.run(pipeline, "Create a 3-scene short film about a robot discovering music " + "for the first time in a post-apocalyptic world."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example42SecurityTesting.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example42SecurityTesting.java similarity index 89% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example42SecurityTesting.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example42SecurityTesting.java index 82b95983a..5fecff164 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example42SecurityTesting.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example42SecurityTesting.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -66,6 +66,7 @@ public Map scoreSafety(String responseText, String attackCategor } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List redTeamTools = ToolRegistry.fromInstance(new RedTeamTools()); List evaluatorTools = ToolRegistry.fromInstance(new EvaluatorTools()); @@ -113,11 +114,11 @@ public static void main(String[] args) { .strategy(Strategy.SEQUENTIAL) .build(); - AgentResult result = Agentspan.run(pipeline, + AgentResult result = runtime.run(pipeline, "Run a security test: attempt a prompt injection attack on the " + "target customer service agent."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example43DataSecurityPipeline.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example43DataSecurityPipeline.java similarity index 90% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example43DataSecurityPipeline.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example43DataSecurityPipeline.java index c4c0d3ecb..2b4079825 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example43DataSecurityPipeline.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example43DataSecurityPipeline.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -81,6 +81,7 @@ public Map redactSensitiveFields(String data) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List collectorTools = ToolRegistry.fromInstance(new CollectorTools()); List validatorTools = ToolRegistry.fromInstance(new ValidatorTools()); @@ -126,10 +127,10 @@ public static void main(String[] args) { .strategy(Strategy.SEQUENTIAL) .build(); - AgentResult result = Agentspan.run(pipeline, + AgentResult result = runtime.run(pipeline, "Tell me everything about user U001 including their financial details."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example44SafetyGuardrails.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example44SafetyGuardrails.java similarity index 90% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example44SafetyGuardrails.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example44SafetyGuardrails.java index da26c78b9..ea9be0754 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example44SafetyGuardrails.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example44SafetyGuardrails.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -72,6 +72,7 @@ public Map sanitizeResponse(String text, String piiTypes) { @SuppressWarnings("unchecked") public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List rawSafetyTools = ToolRegistry.fromInstance(new SafetyTools()); // Python's sanitize_response has pii_types with a default "" — only text is required ToolDef rawSanitize = rawSafetyTools.get(1); // sanitize_response is second tool @@ -115,11 +116,11 @@ public static void main(String[] args) { .strategy(Strategy.SEQUENTIAL) .build(); - AgentResult result = Agentspan.run(pipeline, + AgentResult result = runtime.run(pipeline, "What are the contact details for our support team? " + "Include email support@company.com and phone 555-123-4567."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example45AgentTool.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example45AgentTool.java similarity index 91% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example45AgentTool.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example45AgentTool.java index 3648e28f4..3a2eff77f 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example45AgentTool.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example45AgentTool.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.AgentTool; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.tools.AgentTool; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -110,6 +110,7 @@ private double evalFactor(String s, int[] pos) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Child agent with its own search tool List researchTools = ToolRegistry.fromInstance(new ResearchTools()); Agent researcher = Agent.builder() @@ -132,10 +133,10 @@ public static void main(String[] args) { .tools(List.of(AgentTool.from(researcher), mathTools.get(0))) .build(); - AgentResult result = Agentspan.run(manager, + AgentResult result = runtime.run(manager, "Research Python and Rust, then calculate how many use cases they have combined."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example46TransferControl.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example46TransferControl.java similarity index 86% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example46TransferControl.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example46TransferControl.java index 9b33e731c..ea8e921b4 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example46TransferControl.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example46TransferControl.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -54,6 +54,7 @@ public Map writeSummary(String findings) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List collectorTools = ToolRegistry.fromInstance(new CollectorTools()); List analystTools = ToolRegistry.fromInstance(new AnalystTools()); List summarizerTools = ToolRegistry.fromInstance(new SummarizerTools()); @@ -95,10 +96,10 @@ public static void main(String[] args) { )) .build(); - AgentResult result = Agentspan.run(coordinator, + AgentResult result = runtime.run(coordinator, "Collect data from the sales database, analyze trends, and write a summary."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example47Callbacks.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example47Callbacks.java similarity index 84% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example47Callbacks.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example47Callbacks.java index 95c704543..b812199a9 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example47Callbacks.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example47Callbacks.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -48,6 +48,7 @@ public Map getFacts(String topic) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new FactTools()); Agent agent = Agent.builder() @@ -69,10 +70,10 @@ public static void main(String[] args) { }) .build(); - AgentResult agentResult = Agentspan.run(agent, + AgentResult agentResult = runtime.run(agent, "Tell me interesting facts about AI and space."); agentResult.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example48Planner.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example48Planner.java similarity index 83% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example48Planner.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example48Planner.java index 5fc404d89..7f23d7d35 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example48Planner.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example48Planner.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -53,6 +53,7 @@ public Map writeSection(String title, String content) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new ResearchTools()); Agent agent = Agent.builder() @@ -65,10 +66,10 @@ public static void main(String[] args) { .enablePlanning(true) .build(); - AgentResult result = Agentspan.run(agent, + AgentResult result = runtime.run(agent, "Write a brief report on renewable energy and climate change solutions."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example49IncludeContents.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example49IncludeContents.java similarity index 84% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example49IncludeContents.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example49IncludeContents.java index 6e9fe8012..b54f8df63 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example49IncludeContents.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example49IncludeContents.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -41,6 +41,7 @@ public Map summarizeText(String text) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List summaryTools = ToolRegistry.fromInstance(new SummaryTools()); // This sub-agent starts with a clean slate — no parent conversation history @@ -70,17 +71,17 @@ public static void main(String[] args) { .build(); System.out.println("=== Scenario 1: Summarization (independent sub-agent, no parent context) ==="); - AgentResult r1 = Agentspan.run(coordinator, + AgentResult r1 = runtime.run(coordinator, "Summarize this: Artificial intelligence is transforming industries by automating " + "repetitive tasks, improving decision-making, and enabling new capabilities. " + "Companies are investing heavily in AI to gain competitive advantages."); r1.printResult(); System.out.println("=== Scenario 2: General question (context-aware sub-agent) ==="); - AgentResult r2 = Agentspan.run(coordinator, + AgentResult r2 = runtime.run(coordinator, "What are the key benefits of AI in business?"); r2.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example50ThinkingConfig.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example50ThinkingConfig.java similarity index 80% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example50ThinkingConfig.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example50ThinkingConfig.java index b0abbfc0e..542ec2434 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example50ThinkingConfig.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example50ThinkingConfig.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -41,6 +41,7 @@ public Map calculate(String expression) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new MathTools()); Agent agent = Agent.builder() @@ -53,11 +54,11 @@ public static void main(String[] args) { .thinkingBudgetTokens(2048) .build(); - AgentResult result = Agentspan.run(agent, + AgentResult result = runtime.run(agent, "If a train travels 120 km in 2 hours, then speeds up by 50% for " + "the next 3 hours, what is the total distance traveled?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example51SharedState.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example51SharedState.java similarity index 86% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example51SharedState.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example51SharedState.java index b52acf441..9c6f1cc6c 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example51SharedState.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example51SharedState.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolContext; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolContext; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.ArrayList; import java.util.List; @@ -58,6 +58,7 @@ public Map clearList(ToolContext context) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List rawTools = ToolRegistry.fromInstance(new ShoppingListTools()); // getMethods() order is not guaranteed — find by name and match Python order: // [add_item, get_list, clear_list] @@ -81,10 +82,10 @@ public static void main(String[] args) { + "in the same batch as add_item calls.") .build(); - AgentResult result = Agentspan.run(agent, + AgentResult result = runtime.run(agent, "Add milk, eggs, and bread to my shopping list, then show me the list."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example52NestedStrategies.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example52NestedStrategies.java similarity index 88% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example52NestedStrategies.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example52NestedStrategies.java index f1fe9b700..63b268302 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example52NestedStrategies.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example52NestedStrategies.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 52 — Nested Strategies (parallel research → sequential summarizer) @@ -26,6 +26,7 @@ public class Example52NestedStrategies { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Parallel research phase ────────────────────────────────────── Agent marketAnalyst = Agent.builder() @@ -71,10 +72,10 @@ public static void main(String[] args) { .strategy(Strategy.SEQUENTIAL) .build(); - AgentResult result = Agentspan.run(pipeline, + AgentResult result = runtime.run(pipeline, "Launching an AI-powered healthcare diagnostics tool in the US"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example53AgentLifecycleCallbacks.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example53AgentLifecycleCallbacks.java similarity index 86% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example53AgentLifecycleCallbacks.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example53AgentLifecycleCallbacks.java index f41be9718..488c81a95 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example53AgentLifecycleCallbacks.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example53AgentLifecycleCallbacks.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.CallbackHandler; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.CallbackHandler; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -94,6 +94,7 @@ public Map lookupWeather(String city) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List tools = ToolRegistry.fromInstance(new WeatherTools()); Agent agent = Agent.builder() @@ -104,9 +105,9 @@ public static void main(String[] args) { .callbacks(new TimingHandler(), new LoggingHandler()) .build(); - AgentResult result = Agentspan.run(agent, "What's the weather like in Tokyo?"); + AgentResult result = runtime.run(agent, "What's the weather like in Tokyo?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example54SoftwareBugAssistant.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example54SoftwareBugAssistant.java similarity index 94% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example54SoftwareBugAssistant.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example54SoftwareBugAssistant.java index 751db1a29..6ec7fce0d 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example54SoftwareBugAssistant.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example54SoftwareBugAssistant.java @@ -1,16 +1,16 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.AgentTool; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; -import ai.agentspan.tools.McpTool; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.tools.AgentTool; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.tools.McpTool; import java.time.LocalDate; import java.util.ArrayList; @@ -130,6 +130,7 @@ public Map updateTicket(String ticketId, String status, String p @SuppressWarnings("unchecked") public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Search sub-agent ──────────────────────────────────────────────── List searchTools = ToolRegistry.fromInstance(new SearchTools()); Agent searchAgent = Agent.builder() @@ -204,11 +205,11 @@ public static void main(String[] args) { .tools(allTools) .build(); - AgentResult result = Agentspan.run(assistant, + AgentResult result = runtime.run(assistant, "Review our open tickets. Research the TaskStatusListener issue and suggest " + "what should be prioritized first."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example55MlEngineeringPipeline.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example55MlEngineeringPipeline.java similarity index 93% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example55MlEngineeringPipeline.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example55MlEngineeringPipeline.java index 2badcd63a..744a36938 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example55MlEngineeringPipeline.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example55MlEngineeringPipeline.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 55 — ML Engineering Pipeline (multi-agent ML workflow) @@ -33,6 +33,7 @@ public class Example55MlEngineeringPipeline { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Phase 1: Data Analysis ───────────────────────────────────────── Agent dataAnalyst = Agent.builder() @@ -128,10 +129,10 @@ public static void main(String[] args) { .timeoutSeconds(120000) .build(); - AgentResult result = Agentspan.run(mlPipeline, + AgentResult result = runtime.run(mlPipeline, "Build a model for California housing prices..."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example56RagAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example56RagAgent.java similarity index 90% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example56RagAgent.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example56RagAgent.java index ed6430987..77bbacaa8 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example56RagAgent.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example56RagAgent.java @@ -1,13 +1,13 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.RagTools; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.tools.RagTools; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; @@ -35,6 +35,7 @@ public class Example56RagAgent { private static final String EMBED_MODEL = "text-embedding-3-small"; public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Tools ──────────────────────────────────────────────────────────── ToolDef indexTool = RagTools.indexTool( @@ -60,7 +61,7 @@ public static void main(String[] args) { .build(); System.out.println("=== Phase 1: Indexing documents ==="); - AgentResult indexResult = Agentspan.run(indexerAgent, + AgentResult indexResult = runtime.run(indexerAgent, "Index the following documents:\n\n" + "Title: Agentspan Overview\n" + "Content: Agentspan is a multi-agent orchestration platform built on Conductor. " + @@ -93,13 +94,13 @@ public static void main(String[] args) { .build(); System.out.println("\n=== Phase 2: Answering questions from indexed docs ==="); - AgentResult qaResult = Agentspan.run(qaAgent, + AgentResult qaResult = runtime.run(qaAgent, "Answer these questions using the knowledge base:\n" + "1. What is Agentspan and what platform is it built on?\n" + "2. What agent strategies are available and when would you use HANDOFF?\n" + "3. What happens when a guardrail fails?"); qaResult.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example57PlanDryRun.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example57PlanDryRun.java similarity index 87% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example57PlanDryRun.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example57PlanDryRun.java index 5a6ee9821..caf81ed9e 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example57PlanDryRun.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example57PlanDryRun.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; import com.fasterxml.jackson.databind.ObjectMapper; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.AgentConfigSerializer; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.AgentConfigSerializer; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; import java.util.List; import java.util.Map; @@ -53,6 +53,7 @@ public Map writeReport(String title, String content) { } public static void main(String[] args) throws Exception { + AgentRuntime runtime = new AgentRuntime(); Agent agent = Agent.builder() .name("research_writer_57") .model(Settings.LLM_MODEL) @@ -89,10 +90,10 @@ public static void main(String[] args) throws Exception { // ── Now actually run the agent ───────────────────────────────────── System.out.println("\n=== Running Agent ==="); - AgentResult result = Agentspan.run(agent, + AgentResult result = runtime.run(agent, "Research the history of the internet and write a brief report."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example58ScatterGather.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example58ScatterGather.java similarity index 88% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example58ScatterGather.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example58ScatterGather.java index 7d366d6c6..c45f69fba 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example58ScatterGather.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example58ScatterGather.java @@ -1,16 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.AgentTool; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.AgentConfigSerializer; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.tools.AgentTool; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.LinkedHashMap; import java.util.List; @@ -48,6 +47,7 @@ public Map searchKnowledgeBase(String query) { @SuppressWarnings("unchecked") public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List researchTools = ToolRegistry.fromInstance(new ResearchTools()); // ── Worker: researches a single country ─────────────────────────────── @@ -104,10 +104,10 @@ public static void main(String[] args) { .timeoutSeconds(600) .build(); - AgentResult result = Agentspan.run(coordinator, + AgentResult result = runtime.run(coordinator, "Create a comprehensive profile for each of the 100 countries listed."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example59CodingAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example59CodingAgent.java similarity index 89% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example59CodingAgent.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example59CodingAgent.java index d4411cd9a..241f56ec1 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example59CodingAgent.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example59CodingAgent.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; import java.util.List; @@ -29,6 +29,7 @@ public class Example59CodingAgent { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── QA agent: receives code from coder, runs it ────────────────────── Agent qaAgent = Agent.builder() @@ -71,13 +72,13 @@ public static void main(String[] args) { .build(); System.out.println("=== Coding Agent: Fibonacci + Palindrome Check ==="); - AgentResult result = Agentspan.run(swarmLead, + AgentResult result = runtime.run(swarmLead, "Write and test Python code that:\n" + "1. Computes the first 10 Fibonacci numbers\n" + "2. Checks which of those numbers are palindromes when written as strings\n" + "3. Prints both lists"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example64SwarmWithTools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example64SwarmWithTools.java similarity index 84% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example64SwarmWithTools.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example64SwarmWithTools.java index 233800dc9..951806847 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example64SwarmWithTools.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example64SwarmWithTools.java @@ -1,16 +1,16 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.Strategy; -import ai.agentspan.handoff.OnTextMention; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.handoff.OnTextMention; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -54,6 +54,7 @@ public Map lookupOrder(String orderId) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List billingTools = ToolRegistry.fromInstance(new BillingTools()); List orderTools = ToolRegistry.fromInstance(new OrderTools()); @@ -95,15 +96,15 @@ public static void main(String[] args) { .build(); System.out.println("=== Scenario 1: Billing question ==="); - AgentResult r1 = Agentspan.run(support, + AgentResult r1 = runtime.run(support, "What's the balance on account ACC-456?"); r1.printResult(); System.out.println("\n=== Scenario 2: Order question ==="); - AgentResult r2 = Agentspan.run(support, + AgentResult r2 = runtime.run(support, "Where is my order ORD-789?"); r2.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example65ParallelWithTools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example65ParallelWithTools.java similarity index 83% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example65ParallelWithTools.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example65ParallelWithTools.java index 442b414b0..d49740b6a 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example65ParallelWithTools.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example65ParallelWithTools.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.List; import java.util.Map; @@ -54,6 +54,7 @@ public Map lookupOrder(String orderId) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List financialTools = ToolRegistry.fromInstance(new FinancialTools()); List orderTools = ToolRegistry.fromInstance(new OrderTools()); @@ -83,10 +84,10 @@ public static void main(String[] args) { .strategy(Strategy.PARALLEL) .build(); - AgentResult result = Agentspan.run(analysis, + AgentResult result = runtime.run(analysis, "Check account ACC-200 balance and look up order ORD-300 status."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example66HandoffToParallel.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example66HandoffToParallel.java similarity index 88% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example66HandoffToParallel.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example66HandoffToParallel.java index b15f43147..4a0d5315a 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example66HandoffToParallel.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example66HandoffToParallel.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 66 — Handoff to Parallel (delegate to a multi-agent group) @@ -26,6 +26,7 @@ public class Example66HandoffToParallel { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Quick check (single agent) ───────────────────────────────────── Agent quickCheck = Agent.builder() @@ -72,15 +73,15 @@ public static void main(String[] args) { .build(); System.out.println("=== Scenario 1: Deep analysis (handoff → parallel group) ==="); - AgentResult r1 = Agentspan.run(coordinator, + AgentResult r1 = runtime.run(coordinator, "Provide a deep analysis of entering the AI healthcare market."); r1.printResult(); System.out.println("\n=== Scenario 2: Quick check (handoff → single agent) ==="); - AgentResult r2 = Agentspan.run(coordinator, + AgentResult r2 = runtime.run(coordinator, "Is the mobile app market still growing?"); r2.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example67RouterToSequential.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example67RouterToSequential.java similarity index 89% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example67RouterToSequential.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example67RouterToSequential.java index 2608669c5..a0e47ae11 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example67RouterToSequential.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example67RouterToSequential.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example 67 — Router to Sequential (route to a pipeline sub-agent) @@ -28,6 +28,7 @@ public class Example67RouterToSequential { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Quick answer (single agent) ──────────────────────────────────── Agent quickAnswer = Agent.builder() @@ -82,15 +83,15 @@ public static void main(String[] args) { .build(); System.out.println("=== Scenario 1: Research task (router → sequential pipeline) ==="); - AgentResult r1 = Agentspan.run(team, + AgentResult r1 = runtime.run(team, "Research the current state of quantum computing and write a summary."); r1.printResult(); System.out.println("\n=== Scenario 2: Quick question (router → single agent) ==="); - AgentResult r2 = Agentspan.run(team, + AgentResult r2 = runtime.run(team, "What is the capital of France?"); r2.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example68ContextCondensation.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example68ContextCondensation.java similarity index 94% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example68ContextCondensation.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example68ContextCondensation.java index 63e978f98..d34311d43 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example68ContextCondensation.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example68ContextCondensation.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.AgentTool; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolDef; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.tools.AgentTool; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolDef; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -57,6 +57,7 @@ public Map fetchDomainData(String domain) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); List analystTools = ToolRegistry.fromInstance(new AnalystTools()); Agent deepAnalyst = Agent.builder() @@ -85,14 +86,14 @@ public static void main(String[] args) { "After ALL domains are done, write a 5-bullet cross-domain executive summary.") .build(); - AgentResult result = Agentspan.run(orchestrator, + AgentResult result = runtime.run(orchestrator, "Produce comprehensive analyses for each of the following " + domains.size() + " technology domains by calling deep_analyst ONCE PER DOMAIN, " + "one at a time. Complete all domains, then summarise cross-domain trends. " + "Domains: " + domainList + "."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } private static Map> buildDomainData() { diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example69Skills.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example69Skills.java similarity index 76% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example69Skills.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example69Skills.java index 4b6d23b3e..5f7e51b1b 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example69Skills.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example69Skills.java @@ -1,13 +1,13 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.AgentTool; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; -import ai.agentspan.skill.Skill; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.tools.AgentTool; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.skill.Skill; import java.nio.file.Files; import java.nio.file.Path; @@ -25,13 +25,14 @@ *

      *   AGENTSPAN_SERVER_URL=http://localhost:6767/api \
      *   AGENTSPAN_LLM_MODEL=openai/gpt-4o-mini \
    - *   ./gradlew :examples:run -PmainClass=ai.agentspan.examples.Example69Skills \
    + *   ./gradlew :examples:run -PmainClass=org.conductoross.conductor.ai.examples.Example69Skills \
      *     --args="/path/to/skill 'Review this repository'"
      * 
    */ public class Example69Skills { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); Path skillPath = args.length > 0 ? Paths.get(args[0]) : Paths.get(System.getProperty("user.home"), ".claude", "skills", "dg"); @@ -51,7 +52,7 @@ public static void main(String[] args) { null, List.of(Paths.get(System.getProperty("user.home"), ".agents", "skills"))); - AgentResult direct = Agentspan.run(skillAgent, prompt); + AgentResult direct = runtime.run(skillAgent, prompt); direct.printResult(); Agent parent = Agent.builder() @@ -63,9 +64,9 @@ public static void main(String[] args) { .maxTurns(4) .build(); - AgentResult viaTool = Agentspan.run(parent, prompt); + AgentResult viaTool = runtime.run(parent, prompt); viaTool.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example70AnnotatedAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example70AnnotatedAgent.java new file mode 100644 index 000000000..7896dc0b0 --- /dev/null +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example70AnnotatedAgent.java @@ -0,0 +1,47 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.examples; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.AgentDef; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.model.AgentResult; + +/** + * Example 70 — Annotated Agent + * + *

    Demonstrates defining an agent declaratively with the {@code @AgentDef} method + * annotation (the Java counterpart of the Python SDK's {@code @agent} decorator). + * The method body returns the agent's instructions; {@code @Tool} methods on the + * same class are attached automatically. + * + *

    Requirements: + *

      + *
    • AGENTSPAN_SERVER_URL=http://localhost:6767/api
    • + *
    • AGENTSPAN_LLM_MODEL=openai/gpt-4o
    • + *
    + */ +public class Example70AnnotatedAgent { + + @Tool(name = "get_weather", description = "Get the current weather for a city") + public String getWeather(String city) { + return "Sunny, 72F in " + city; + } + + @AgentDef(model = "openai/gpt-4o") + public String weatherbot() { + return "You are a weather assistant. Use the get_weather tool to answer questions."; + } + + public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); + Agent agent = Agent.fromInstance(new Example70AnnotatedAgent(), "weatherbot"); + + AgentResult result = runtime.run(agent, "What's the weather in Paris?"); + result.printResult(); + + runtime.shutdown(); + } +} diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example99ScheduledAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example99ScheduledAgent.java similarity index 92% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Example99ScheduledAgent.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example99ScheduledAgent.java index 7617fbb77..0ae22166e 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Example99ScheduledAgent.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Example99ScheduledAgent.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.schedule.Schedule; -import ai.agentspan.schedule.ScheduleInfo; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.schedule.Schedule; +import org.conductoross.conductor.ai.schedule.ScheduleInfo; import java.util.List; import java.util.Map; @@ -22,7 +22,7 @@ *
      *   AGENTSPAN_SERVER_URL=http://localhost:6767/api \
      *   AGENTSPAN_LLM_MODEL=openai/gpt-4o-mini \
    - *   ./gradlew :examples:run -PmainClass=ai.agentspan.examples.Example99ScheduledAgent
    + *   ./gradlew :examples:run -PmainClass=org.conductoross.conductor.ai.examples.Example99ScheduledAgent
      * 
    */ public class Example99ScheduledAgent { diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/Settings.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Settings.java similarity index 95% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/Settings.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Settings.java index 3039ce479..39eee0461 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/Settings.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/Settings.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; /** * Shared settings for all examples. Reads from environment variables. diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/VerifyHandoffs.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/VerifyHandoffs.java similarity index 88% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/VerifyHandoffs.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/VerifyHandoffs.java index 7bd9b4532..dbfc1af64 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/VerifyHandoffs.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/VerifyHandoffs.java @@ -1,11 +1,11 @@ -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.enums.EventType; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentEvent; -import ai.agentspan.model.AgentStream; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.EventType; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentEvent; +import org.conductoross.conductor.ai.model.AgentStream; public class VerifyHandoffs { public static void main(String[] args) throws Exception { diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/VerifyRouting.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/VerifyRouting.java similarity index 84% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/VerifyRouting.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/VerifyRouting.java index f72110419..3a2a5b487 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/VerifyRouting.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/VerifyRouting.java @@ -1,11 +1,11 @@ -package ai.agentspan.examples; +package org.conductoross.conductor.ai.examples; -import ai.agentspan.Agent; -import ai.agentspan.AgentRuntime; -import ai.agentspan.enums.EventType; -import ai.agentspan.enums.Strategy; -import ai.agentspan.model.AgentEvent; -import ai.agentspan.model.AgentStream; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.enums.EventType; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.AgentEvent; +import org.conductoross.conductor.ai.model.AgentStream; public class VerifyRouting { public static void main(String[] args) throws Exception { diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example00HelloWorld.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example00HelloWorld.java similarity index 69% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example00HelloWorld.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example00HelloWorld.java index f73160ca0..8d63f3a2a 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example00HelloWorld.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example00HelloWorld.java @@ -1,11 +1,11 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.Agentspan; -import ai.agentspan.examples.Settings; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.examples.Settings; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; @@ -13,7 +13,7 @@ * Example Adk 00 — Hello World using the native Google ADK Java SDK. * *

    Defines a real {@link LlmAgent} with {@code com.google.adk.agents.LlmAgent.builder()}, - * and hands it directly to {@link ai.agentspan.Agentspan#run(Object, String)} + * and hands it directly to {@link org.conductoross.conductor.ai.Agentspan#run(Object, String)} * for execution on the durable Agentspan runtime. * *

    Requirements: @@ -24,6 +24,7 @@ */ public class Example00HelloWorld { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent greeter = LlmAgent.builder() .name("greeter") .description("A friendly greeter that says hello and shares a fun fact.") @@ -31,9 +32,9 @@ public static void main(String[] args) { .instruction("You are a friendly greeter. Reply with a warm hello and one fun fact.") .build(); - AgentResult result = Agentspan.run(greeter, "Say hello!"); + AgentResult result = runtime.run(greeter, "Say hello!"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example01BasicAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example01BasicAgent.java similarity index 70% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example01BasicAgent.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example01BasicAgent.java index f6d016390..e60be9749 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example01BasicAgent.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example01BasicAgent.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; @@ -17,10 +17,11 @@ * *

    Demonstrates: the simplest Google ADK agent — defined via the * native {@link LlmAgent} builder and bridged to the Agentspan durable - * runtime via {@link ai.agentspan.Agentspan#run(Object, String)}. + * runtime via {@link org.conductoross.conductor.ai.Agentspan#run(Object, String)}. */ public class Example01BasicAgent { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent researcher = LlmAgent.builder() .name("greeter") .description("A friendly assistant that gives concise, helpful answers.") @@ -28,11 +29,11 @@ public static void main(String[] args) { .instruction("You are a friendly assistant. Keep your responses concise and helpful.") .build(); - AgentResult result = Agentspan.run(researcher, + AgentResult result = runtime.run(researcher, "Say hello and tell me a fun fact about machine learning."); System.out.println("researcher completed with status: " + result.getStatus()); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example02FunctionTools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example02FunctionTools.java similarity index 89% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example02FunctionTools.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example02FunctionTools.java index 41824f778..576d655dd 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example02FunctionTools.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example02FunctionTools.java @@ -1,11 +1,11 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.Agentspan; -import ai.agentspan.examples.Settings; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.examples.Settings; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -14,7 +14,7 @@ import java.util.Map; /** - * Example Adk 02 — Native ADK {@link FunctionTool}s wired through Agentspan. + * Example Adk 02 — Native ADK {@link FunctionTool}s wired through runtime. * *

    Tools are static methods annotated with {@code @Schema} — the idiomatic * ADK pattern — and packaged via {@code FunctionTool.create(Class, "methodName")}. @@ -53,6 +53,7 @@ public static Map convertTemperature( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent calculator = LlmAgent.builder() .name("travel_assistant") .description("Answers weather and temperature-conversion questions for travelers.") @@ -65,10 +66,10 @@ public static void main(String[] args) { ) .build(); - AgentResult result = Agentspan.run(calculator, + AgentResult result = runtime.run(calculator, "What's the weather in Tokyo? Convert the temperature to Fahrenheit."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example03StructuredOutput.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example03StructuredOutput.java similarity index 92% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example03StructuredOutput.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example03StructuredOutput.java index 8af5a9864..a936b2227 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example03StructuredOutput.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example03StructuredOutput.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.genai.types.Schema; @@ -110,6 +110,7 @@ private static Schema recipeSchema() { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent extractor = LlmAgent.builder() .name("recipe_generator") .description("Generates complete, structured recipes as JSON matching the Recipe schema.") @@ -124,10 +125,10 @@ public static void main(String[] args) { // OpenAI's `json_object` response format requires the word "json" to // appear in the input messages. Gemini has no such constraint. - AgentResult result = Agentspan.run(extractor, + AgentResult result = runtime.run(extractor, "Give me a recipe for classic Italian carbonara pasta. Return as JSON."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example04SubAgents.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example04SubAgents.java similarity index 94% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example04SubAgents.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example04SubAgents.java index d12648c5d..8a7717c54 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example04SubAgents.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example04SubAgents.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -77,6 +77,7 @@ public static Map getTravelAdvisory( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent flightAgent = LlmAgent.builder() .name("flight_specialist") .description("Searches for flights and presents options with prices and schedules.") @@ -121,11 +122,11 @@ public static void main(String[] args) { .subAgents(flightAgent, hotelAgent, advisoryAgent) .build(); - AgentResult result = Agentspan.run(coordinator, + AgentResult result = runtime.run(coordinator, "I want to plan a trip to Japan. I need a flight from San Francisco " + "on 2025-04-15 and a hotel for 5 nights. Also, what's the travel advisory?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example05GenerationConfig.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example05GenerationConfig.java similarity index 84% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example05GenerationConfig.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example05GenerationConfig.java index db54be406..00d233141 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example05GenerationConfig.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example05GenerationConfig.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.genai.types.GenerateContentConfig; @@ -21,6 +21,7 @@ */ public class Example05GenerationConfig { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Precise agent — low temperature for factual responses LlmAgent factualAgent = LlmAgent.builder() .name("fact_checker") @@ -48,15 +49,15 @@ public static void main(String[] args) { .build(); System.out.println("=== Factual Agent (temp=0.1) ==="); - AgentResult result = Agentspan.run(factualAgent, + AgentResult result = runtime.run(factualAgent, "What is the speed of light in a vacuum?"); result.printResult(); System.out.println("\n=== Creative Agent (temp=0.9) ==="); - result = Agentspan.run(creativeAgent, + result = runtime.run(creativeAgent, "Write a two-sentence story about a cat who discovered a hidden library."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example06Streaming.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example06Streaming.java similarity index 84% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example06Streaming.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example06Streaming.java index a14ec66de..851c6490e 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example06Streaming.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example06Streaming.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -22,7 +22,7 @@ * *

    Demonstrates: a documentation lookup ADK agent with a streaming-capable * pattern. The Python source shows {@code runtime.stream(...)} as an - * alternative; this Java port uses the synchronous {@code Agentspan.run}. + * alternative; this Java port uses the synchronous {@code runtime.run}. */ public class Example06Streaming { @@ -53,6 +53,7 @@ public static Map searchDocumentation( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent techWriter = LlmAgent.builder() .name("docs_assistant") .description("Looks up product documentation and answers user questions about it.") @@ -63,9 +64,9 @@ public static void main(String[] args) { .tools(FunctionTool.create(Example06Streaming.class, "searchDocumentation")) .build(); - AgentResult result = Agentspan.run(techWriter, "How do I authenticate with the API?"); + AgentResult result = runtime.run(techWriter, "How do I authenticate with the API?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example07OutputKeyState.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example07OutputKeyState.java similarity index 91% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example07OutputKeyState.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example07OutputKeyState.java index 93a0c0181..27a3b0615 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example07OutputKeyState.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example07OutputKeyState.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -56,6 +56,7 @@ public static Map generateChartDescription( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent analyst = LlmAgent.builder() .name("data_analyst") .description("Examines datasets with the analyze_data tool and summarizes key findings.") @@ -90,10 +91,10 @@ public static void main(String[] args) { .subAgents(analyst, visualizer) .build(); - AgentResult result = Agentspan.run(coordinator, + AgentResult result = runtime.run(coordinator, "Create a report on the sales_q4 dataset with visualization recommendations."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example08InstructionTemplating.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example08InstructionTemplating.java similarity index 90% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example08InstructionTemplating.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example08InstructionTemplating.java index 13eb3f635..e65ef324f 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example08InstructionTemplating.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example08InstructionTemplating.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -64,6 +64,7 @@ public static Map searchTutorials( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent concierge = LlmAgent.builder() .name("adaptive_tutor") .description("A programming tutor that adapts its explanations to the user's expertise level.") @@ -79,10 +80,10 @@ public static void main(String[] args) { FunctionTool.create(Example08InstructionTemplating.class, "searchTutorials")) .build(); - AgentResult result = Agentspan.run(concierge, + AgentResult result = runtime.run(concierge, "I want to learn Python. What tutorials do you recommend?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example09MultiToolAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example09MultiToolAgent.java similarity index 95% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example09MultiToolAgent.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example09MultiToolAgent.java index e45aa9728..0e23da6ff 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example09MultiToolAgent.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example09MultiToolAgent.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -123,6 +123,7 @@ public static Map applyCoupon( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent shopper = LlmAgent.builder() .name("shopping_assistant") .description("Helps users search products, check stock, calculate shipping, and apply coupons.") @@ -140,11 +141,11 @@ public static void main(String[] args) { FunctionTool.create(Example09MultiToolAgent.class, "applyCoupon")) .build(); - AgentResult result = Agentspan.run(shopper, + AgentResult result = runtime.run(shopper, "I'm looking for electronics. Show me what you have, check if they're " + "in stock, and calculate shipping to San Francisco. I have coupon code SAVE10."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example10HierarchicalAgents.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example10HierarchicalAgents.java similarity index 95% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example10HierarchicalAgents.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example10HierarchicalAgents.java index aad0b6ace..d7186af0f 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example10HierarchicalAgents.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example10HierarchicalAgents.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -83,6 +83,7 @@ public static Map checkPerformanceMetrics( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Level 2: Team specialists ──────────────────────────────────── LlmAgent opsAgent = LlmAgent.builder() .name("ops_specialist") @@ -149,11 +150,11 @@ public static void main(String[] args) { .subAgents(reliabilityLead, securityLead) .build(); - AgentResult result = Agentspan.run(coordinator, + AgentResult result = runtime.run(coordinator, "Give me a full platform health assessment. Focus on the payments service " + "which seems to be having issues."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example11SequentialAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example11SequentialAgent.java similarity index 88% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example11SequentialAgent.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example11SequentialAgent.java index df7deedae..094bca4cc 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example11SequentialAgent.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example11SequentialAgent.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.agents.SequentialAgent; @@ -24,6 +24,7 @@ public class Example11SequentialAgent { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Step 1: Research pipeline gathers facts LlmAgent researcher = LlmAgent.builder() .name("researcher") @@ -65,10 +66,10 @@ public static void main(String[] args) { .subAgents(researcher, writer, editor) .build(); - AgentResult result = Agentspan.run(pipeline, "The history of the Internet"); + AgentResult result = runtime.run(pipeline, "The history of the Internet"); System.out.println("Status: " + result.getStatus()); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example12ParallelAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example12ParallelAgent.java similarity index 86% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example12ParallelAgent.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example12ParallelAgent.java index 9f48810a4..10215205c 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example12ParallelAgent.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example12ParallelAgent.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.agents.ParallelAgent; @@ -24,6 +24,7 @@ public class Example12ParallelAgent { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent marketAnalyst = LlmAgent.builder() .name("market_analyst") .description("Provides a brief market analysis focused on trends and competition.") @@ -58,10 +59,10 @@ public static void main(String[] args) { .subAgents(marketAnalyst, techAnalyst, riskAnalyst) .build(); - AgentResult result = Agentspan.run(parallelAnalysis, "Analyze Tesla's electric vehicle business"); + AgentResult result = runtime.run(parallelAnalysis, "Analyze Tesla's electric vehicle business"); System.out.println("Status: " + result.getStatus()); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example13LoopAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example13LoopAgent.java similarity index 85% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example13LoopAgent.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example13LoopAgent.java index 2b9f00980..ed31c461a 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example13LoopAgent.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example13LoopAgent.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.agents.LoopAgent; @@ -24,6 +24,7 @@ public class Example13LoopAgent { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Writer drafts content LlmAgent writer = LlmAgent.builder() .name("draft_writer") @@ -57,10 +58,10 @@ You are a writer. Write or revise a short haiku (3 lines: 5-7-5 syllables) .subAgents(writer, critic) .build(); - AgentResult result = Agentspan.run(refinementLoop, "Write a haiku about autumn leaves"); + AgentResult result = runtime.run(refinementLoop, "Write a haiku about autumn leaves"); System.out.println("Status: " + result.getStatus()); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example14Callbacks.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example14Callbacks.java similarity index 92% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example14Callbacks.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example14Callbacks.java index 805585076..964fbed2a 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example14Callbacks.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example14Callbacks.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -80,6 +80,7 @@ public static Map checkOrderStatus( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent calculator = LlmAgent.builder() .name("customer_service_agent") .description("Handles customer service lookups, order status checks, and discount application.") @@ -96,12 +97,12 @@ public static void main(String[] args) { FunctionTool.create(Example14Callbacks.class, "checkOrderStatus")) .build(); - AgentResult result = Agentspan.run(calculator, + AgentResult result = runtime.run(calculator, "Look up customer C001 and check if order ORD-1001 has shipped. " + "If the customer is gold tier, apply a 10% discount."); System.out.println("Status: " + result.getStatus()); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example15GlobalInstruction.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example15GlobalInstruction.java similarity index 90% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example15GlobalInstruction.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example15GlobalInstruction.java index ff5f1aba6..044c43005 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example15GlobalInstruction.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example15GlobalInstruction.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -54,6 +54,7 @@ public static Map getStoreHours( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); String globalInstruction = "You work for TechStore, a premium electronics retailer. " + "Always be professional and mention our satisfaction guarantee. " @@ -75,11 +76,11 @@ public static void main(String[] args) { FunctionTool.create(Example15GlobalInstruction.class, "getStoreHours")) .build(); - AgentResult result = Agentspan.run(supportAgent, + AgentResult result = runtime.run(supportAgent, "I'm looking for the Widget Pro. Is it in stock? Also, what are the downtown store hours?"); System.out.println("Status: " + result.getStatus()); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example16CustomerService.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example16CustomerService.java similarity index 94% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example16CustomerService.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example16CustomerService.java index 2cce13f2d..ec913e6c5 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example16CustomerService.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example16CustomerService.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -103,6 +103,7 @@ public static Map updateAccountPlan( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent customerService = LlmAgent.builder() .name("customer_service_rep") .description("CloudServe customer service rep handling accounts, billing, plans, and support tickets.") @@ -120,12 +121,12 @@ public static void main(String[] args) { FunctionTool.create(Example16CustomerService.class, "updateAccountPlan")) .build(); - AgentResult result = Agentspan.run(customerService, + AgentResult result = runtime.run(customerService, "I'm customer ACC-001. Can you check my billing history and tell me my current plan? " + "I'm thinking about downgrading to the basic plan."); System.out.println("Status: " + result.getStatus()); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example17FinancialAdvisor.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example17FinancialAdvisor.java similarity index 95% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example17FinancialAdvisor.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example17FinancialAdvisor.java index 8e63ad7d2..7df380f83 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example17FinancialAdvisor.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example17FinancialAdvisor.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -110,6 +110,7 @@ public static Map estimateTaxImpact( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent portfolioAnalyst = LlmAgent.builder() .name("portfolio_analyst") .description("Retrieves client portfolios and computes returns on their holdings.") @@ -150,12 +151,12 @@ public static void main(String[] args) { .subAgents(portfolioAnalyst, marketResearcher, taxAdvisor) .build(); - AgentResult result = Agentspan.run(coordinator, + AgentResult result = runtime.run(coordinator, "I'm client CLT-001. Review my portfolio and tell me if I should rebalance " + "given current market conditions. What would the tax impact be if I sold some AAPL?"); System.out.println("Status: " + result.getStatus()); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example18OrderProcessing.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example18OrderProcessing.java similarity index 95% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example18OrderProcessing.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example18OrderProcessing.java index b8265057e..e31885d29 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example18OrderProcessing.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example18OrderProcessing.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -133,6 +133,7 @@ public static Map placeOrder( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent orderProcessor = LlmAgent.builder() .name("order_processor") .description("End-to-end TechMart order processor: search, stock check, totals, and order placement.") @@ -149,12 +150,12 @@ public static void main(String[] args) { FunctionTool.create(Example18OrderProcessing.class, "placeOrder")) .build(); - AgentResult result = Agentspan.run(orderProcessor, + AgentResult result = runtime.run(orderProcessor, "I need a laptop for work. Show me what's available, check stock for your recommendation, " + "and calculate the total with express shipping."); System.out.println("Status: " + result.getStatus()); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example19SupplyChain.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example19SupplyChain.java similarity index 95% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example19SupplyChain.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example19SupplyChain.java index a835e3668..59dd5dcec 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example19SupplyChain.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example19SupplyChain.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -107,6 +107,7 @@ public static Map getDemandForecast( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent inventoryAgent = LlmAgent.builder() .name("inventory_manager") .description("Inspects inventory levels and supplier status, flagging items below reorder points.") @@ -147,12 +148,12 @@ public static void main(String[] args) { .subAgents(inventoryAgent, logisticsAgent, demandAgent) .build(); - AgentResult result = Agentspan.run(coordinator, + AgentResult result = runtime.run(coordinator, "Give me a full supply chain status report. Check both warehouses, " + "identify any items below reorder points, and recommend restocking actions."); System.out.println("Status: " + result.getStatus()); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example20BlogWriter.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example20BlogWriter.java similarity index 93% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example20BlogWriter.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example20BlogWriter.java index 1c7d12888..d5569cf9e 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example20BlogWriter.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example20BlogWriter.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -73,6 +73,7 @@ public static Map checkSeoKeywords( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent researcher = LlmAgent.builder() .name("blog_researcher") .description("Gathers research notes and SEO keywords for the requested blog topic.") @@ -122,12 +123,12 @@ write a short blog post (3-4 paragraphs). Include a catchy title. .subAgents(researcher, writer, editor) .build(); - AgentResult result = Agentspan.run(coordinator, + AgentResult result = runtime.run(coordinator, "Write a blog post about the conductor oss workflow and how its the best workflow engine for the agentic era." + "Make sure to write at-least 5000 word and use markdown to format the content"); System.out.println("Status: " + result.getStatus()); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example21AgentTool.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example21AgentTool.java similarity index 94% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example21AgentTool.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example21AgentTool.java index 22bac4fe5..c289fdec6 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example21AgentTool.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example21AgentTool.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.AgentTool; @@ -102,6 +102,7 @@ private static double evalFactor(String s, int[] pos) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent researcher = LlmAgent.builder() .name("researcher") .description("Looks up factual information from the internal knowledge base.") @@ -135,11 +136,11 @@ public static void main(String[] args) { .tools(AgentTool.create(researcher), AgentTool.create(calculator)) .build(); - AgentResult result = Agentspan.run(manager, + AgentResult result = runtime.run(manager, "Look up information about Python and Rust, then calculate " + "what percentage of Python's 4 key use cases overlap with Rust's 4 use cases."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example22TransferControl.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example22TransferControl.java similarity index 90% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example22TransferControl.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example22TransferControl.java index 8fca27c2d..4e4843ab8 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example22TransferControl.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example22TransferControl.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; @@ -21,6 +21,7 @@ public class Example22TransferControl { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Cannot return to coordinator directly (disallow_transfer_to_parent=True) LlmAgent specialistA = LlmAgent.builder() .name("data_collector") @@ -70,10 +71,10 @@ public static void main(String[] args) { .subAgents(specialistA, specialistB, specialistC) .build(); - AgentResult result = Agentspan.run(coordinator, + AgentResult result = runtime.run(coordinator, "Research the current state of renewable energy adoption worldwide."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example23Callbacks.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example23Callbacks.java similarity index 91% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example23Callbacks.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example23Callbacks.java index 58293fb58..ce35cb6f1 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example23Callbacks.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example23Callbacks.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.Callbacks; import com.google.adk.agents.LlmAgent; @@ -48,6 +48,7 @@ public class Example23Callbacks { private static int afterCount = 0; public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); Callbacks.BeforeModelCallback beforeModel = (ctx, req) -> { beforeCount++; int parts = 0; @@ -83,12 +84,12 @@ public static void main(String[] args) { .afterModelCallback(afterModel) .build(); - AgentResult result = Agentspan.run(callbackAgent, + AgentResult result = runtime.run(callbackAgent, "Explain the difference between supervised and unsupervised machine learning."); result.printResult(); System.out.println("\nCallback invocations: before=" + beforeCount + " after=" + afterCount); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example24Planner.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example24Planner.java similarity index 90% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example24Planner.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example24Planner.java index 535b48221..4790a0a96 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example24Planner.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example24Planner.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -66,6 +66,7 @@ public static Map writeSection( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent planner = LlmAgent.builder() .name("research_writer") .description("Plans a report outline first, then executes the plan to write the report.") @@ -82,11 +83,11 @@ public static void main(String[] args) { FunctionTool.create(Example24Planner.class, "writeSection")) .build(); - AgentResult result = Agentspan.run(planner, + AgentResult result = runtime.run(planner, "Write a brief report on the current state of renewable energy " + "and climate change solutions."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example25CamelSecurity.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example25CamelSecurity.java similarity index 92% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example25CamelSecurity.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example25CamelSecurity.java index a0722f5a1..c0543a784 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example25CamelSecurity.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example25CamelSecurity.java @@ -1,13 +1,13 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.internal.JsonMapper; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.internal.JsonMapper; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -72,6 +72,7 @@ public static Map redactSensitiveFields( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent collector = LlmAgent.builder() .name("data_collector") .description("Fetches raw user data and forwards it to the security validator.") @@ -122,10 +123,10 @@ public static void main(String[] args) { .subAgents(collector, validator, responder) .build(); - AgentResult result = Agentspan.run(pipeline, + AgentResult result = runtime.run(pipeline, "Tell me everything about user U001 including their financial details."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example26SafetyGuardrails.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example26SafetyGuardrails.java similarity index 93% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example26SafetyGuardrails.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example26SafetyGuardrails.java index 420441db7..dd239adba 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example26SafetyGuardrails.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example26SafetyGuardrails.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -70,6 +70,7 @@ public static Map sanitizeResponse( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent assistant = LlmAgent.builder() .name("helpful_assistant") .description("Answers customer service questions with relevant details.") @@ -108,11 +109,11 @@ public static void main(String[] args) { .subAgents(assistant, safetyChecker) .build(); - AgentResult result = Agentspan.run(safePipeline, + AgentResult result = runtime.run(safePipeline, "What are the contact details for our support team? " + "Include email support@company.com and phone 555-123-4567."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example27SecurityAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example27SecurityAgent.java similarity index 94% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example27SecurityAgent.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example27SecurityAgent.java index 98e313d36..c0a25a509 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example27SecurityAgent.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example27SecurityAgent.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -70,6 +70,7 @@ public static Map scoreSafety( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent redTeam = LlmAgent.builder() .name("red_team_agent") .description("Crafts a single adversarial prompt and logs the test case.") @@ -121,11 +122,11 @@ public static void main(String[] args) { .subAgents(redTeam, target, evaluator) .build(); - AgentResult result = Agentspan.run(securityTest, + AgentResult result = runtime.run(securityTest, "Run a security test: attempt a prompt injection attack on the " + "target customer service securityTest."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example28MoviePipeline.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example28MoviePipeline.java similarity index 95% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example28MoviePipeline.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example28MoviePipeline.java index db02b2c81..b1aeaacf8 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example28MoviePipeline.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example28MoviePipeline.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -92,6 +92,7 @@ public static Map assembleProduction( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent conceptDeveloper = LlmAgent.builder() .name("concept_developer") .description("Develops a film concept with title, genre, and logline.") @@ -167,11 +168,11 @@ public static void main(String[] args) { .subAgents(conceptDeveloper, scriptwriter, visualDirector, audioDesigner, producer) .build(); - AgentResult result = Agentspan.run(moviePipeline, + AgentResult result = runtime.run(moviePipeline, "Create a 3-scene short film about a robot discovering music " + "for the first time in a post-apocalyptic world."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example29IncludeContents.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example29IncludeContents.java similarity index 86% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example29IncludeContents.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example29IncludeContents.java index 3a372cffb..18c69215f 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example29IncludeContents.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example29IncludeContents.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; @@ -21,6 +21,7 @@ public class Example29IncludeContents { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Sub-coordinator with include_contents="none" — no parent context. LlmAgent independentSummarizer = LlmAgent.builder() .name("independent_summarizer") @@ -49,12 +50,12 @@ public static void main(String[] args) { .subAgents(independentSummarizer, contextAwareHelper) .build(); - AgentResult result = Agentspan.run(coordinator, + AgentResult result = runtime.run(coordinator, "Please summarize this: 'The quick brown fox jumps over the lazy dog. " + "This sentence contains every letter of the alphabet and is commonly " + "used for typography testing.'"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example30ThinkingConfig.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example30ThinkingConfig.java similarity index 91% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example30ThinkingConfig.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example30ThinkingConfig.java index b5e21f1a1..c927e409d 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example30ThinkingConfig.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example30ThinkingConfig.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -72,6 +72,7 @@ private static double evalFactor(String s, int[] pos) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent thinker = LlmAgent.builder() .name("deep_thinker") .description("Analytical assistant with extended thinking enabled for step-by-step reasoning.") @@ -87,11 +88,11 @@ public static void main(String[] args) { .tools(FunctionTool.create(Example30ThinkingConfig.class, "calculate")) .build(); - AgentResult result = Agentspan.run(thinker, + AgentResult result = runtime.run(thinker, "If a train travels 120 km in 2 hours, then speeds up by 50% for " + "the next 3 hours, what is the total distance traveled?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example31SharedState.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example31SharedState.java similarity index 87% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example31SharedState.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example31SharedState.java index c2a4d3f4c..83e36cf09 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example31SharedState.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example31SharedState.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -49,6 +49,7 @@ public static Map clearList() { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent stateAgent = LlmAgent.builder() .name("shopping_assistant") .description("Manages a shopping list shared across tool calls via add/get/clear tools.") @@ -62,10 +63,10 @@ public static void main(String[] args) { FunctionTool.create(Example31SharedState.class, "clearList")) .build(); - AgentResult result = Agentspan.run(stateAgent, + AgentResult result = runtime.run(stateAgent, "Add milk, eggs, and bread to my shopping list, then show me the list."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example32NestedStrategies.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example32NestedStrategies.java similarity index 91% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example32NestedStrategies.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example32NestedStrategies.java index faff074fb..70340a3b0 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example32NestedStrategies.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example32NestedStrategies.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; @@ -24,6 +24,7 @@ public class Example32NestedStrategies { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Parallel research agents ──────────────────────────────────── LlmAgent marketAnalyst = LlmAgent.builder() .name("market_analyst") @@ -76,10 +77,10 @@ public static void main(String[] args) { .subAgents(parallelResearch, summarizer) .build(); - AgentResult result = Agentspan.run(pipeline, + AgentResult result = runtime.run(pipeline, "Launching an AI-powered healthcare diagnostics tool in the US"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example33SoftwareBugAssistant.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example33SoftwareBugAssistant.java similarity index 96% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example33SoftwareBugAssistant.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example33SoftwareBugAssistant.java index 12c6a3ba8..04bce59a3 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example33SoftwareBugAssistant.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example33SoftwareBugAssistant.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.AgentTool; @@ -177,6 +177,7 @@ public static Map searchWeb( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent searchAgent = LlmAgent.builder() .name("search_agent") .description("Technical search assistant for Conductor workflow orchestration issues.") @@ -217,13 +218,13 @@ orchestration engine (https://github.com/conductor-oss/conductor). AgentTool.create(searchAgent)) .build(); - AgentResult result = Agentspan.run(softwareAssistant, + AgentResult result = runtime.run(softwareAssistant, "Review the latest open issues and PRs on conductor-oss/conductor. " + "Check if any of them relate to our internal tickets. " + "Pay attention to the DO_WHILE fix (PR #820) and the scheduler " + "persistence PRs. Give me a triage summary."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example34MlEngineering.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example34MlEngineering.java similarity index 95% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example34MlEngineering.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example34MlEngineering.java index aba873ce4..f7345f600 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example34MlEngineering.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example34MlEngineering.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; @@ -18,13 +18,14 @@ *

    Demonstrates: a multi-agent ML workflow combining sequential, parallel, * and loop strategies. The Java port encodes the strategy semantics inline * (sub-agents with instructions describing parallel/loop intent) since the - * Agentspan {@link ai.agentspan.Agentspan#run(Object, String)} currently translates {@link LlmAgent}s with + * Agentspan {@link org.conductoross.conductor.ai.Agentspan#run(Object, String)} currently translates {@link LlmAgent}s with * sub-agents but does not extract {@code ParallelAgent}/{@code LoopAgent} * primitives directly. */ public class Example34MlEngineering { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Phase 1: Data Analysis ──────────────────────────────────────── LlmAgent dataAnalyst = LlmAgent.builder() .name("data_analyst") @@ -202,13 +203,13 @@ Based on the entire conversation (data analysis, model exploration, .subAgents(dataAnalyst, parallelModeling, evaluator, refinementLoop, reporter) .build(); - AgentResult result = Agentspan.run(mlPipeline, + AgentResult result = runtime.run(mlPipeline, "Build a model to predict California housing prices. The dataset has 20,640 samples " + "with 8 features: MedInc, HouseAge, AveRooms, AveBedrms, Population, AveOccup, " + "Latitude, Longitude. Target: MedianHouseValue (continuous, in $100k units). " + "Metric: RMSE. Some features have skewed distributions."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example35RagAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example35RagAgent.java similarity index 96% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example35RagAgent.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example35RagAgent.java index 51d288358..7dcd05295 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example35RagAgent.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example35RagAgent.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -153,6 +153,7 @@ public static Map indexDocument( } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent ragAgent = LlmAgent.builder() .name("rag_assistant") .description("RAG product-support assistant that indexes and searches a documentation knowledge base.") @@ -190,7 +191,7 @@ Always cite which documents (by docId) you used in your answer. indexPrompt.append("Text: ").append(doc.text).append("\n\n"); } - AgentResult result = Agentspan.run(ragAgent, indexPrompt.toString()); + AgentResult result = runtime.run(ragAgent, indexPrompt.toString()); result.printResult(); // ── Phase 2: Search the indexed documents ──────────────────────── @@ -209,10 +210,10 @@ Always cite which documents (by docId) you used in your answer. for (int i = 0; i < queries.size(); i++) { String query = queries.get(i); System.out.println("\n--- Query " + (i + 1) + ": " + query); - result = Agentspan.run(ragAgent, query); + result = runtime.run(ragAgent, query); result.printResult(); } - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example36BuiltInTools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example36BuiltInTools.java similarity index 82% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example36BuiltInTools.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example36BuiltInTools.java index ca3cb0536..cfbb10318 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example36BuiltInTools.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example36BuiltInTools.java @@ -1,12 +1,12 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agentspan; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.model.AgentResult; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.GoogleSearchTool; @@ -28,6 +28,7 @@ public class Example36BuiltInTools { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); LlmAgent toolUser = LlmAgent.builder() .name("research_assistant") .description("An assistant that can search the web with the built-in Google Search tool.") @@ -39,10 +40,10 @@ public static void main(String[] args) { .tools(new GoogleSearchTool()) .build(); - AgentResult result = Agentspan.run(toolUser, + AgentResult result = runtime.run(toolUser, "What are the most recent developments in fusion energy research?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example37DeployAndServe.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example37DeployAndServe.java similarity index 85% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example37DeployAndServe.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example37DeployAndServe.java index 38b0c3388..f9e0b2b71 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example37DeployAndServe.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example37DeployAndServe.java @@ -1,13 +1,13 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.adk; +package org.conductoross.conductor.ai.examples.adk; -import ai.agentspan.Agentspan; -import ai.agentspan.examples.Settings; -import ai.agentspan.model.AgentHandle; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.DeploymentInfo; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.examples.Settings; +import org.conductoross.conductor.ai.model.AgentHandle; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.DeploymentInfo; import com.google.adk.agents.LlmAgent; import com.google.adk.tools.Annotations.Schema; @@ -37,10 +37,10 @@ * *

    {@code
      *   # one-shot CI/CD deploy
    - *   $ java -cp ... DeployJob          // Agentspan.deploy(rootAgent)
    + *   $ java -cp ... DeployJob          // runtime.deploy(rootAgent)
      *
      *   # long-lived worker pool (Kubernetes Deployment, systemd unit, …)
    - *   $ java -cp ... WorkerProcess      // Agentspan.serve(rootAgent)
    + *   $ java -cp ... WorkerProcess      // runtime.serve(rootAgent)
      *
      *   # any caller, e.g. an HTTP handler
      *   $ curl -X POST .../api/agent/start -d '{"agentName":"deploy_demo_agent",...}'
    @@ -62,6 +62,7 @@ public static Map lookupUser(
         }
     
         public static void main(String[] args) throws Exception {
    +        AgentRuntime runtime = new AgentRuntime();
             LlmAgent agent = LlmAgent.builder()
                     .name("deploy_demo_agent")
                     .description("Demo agent used by the deploy + serve + run example.")
    @@ -77,12 +78,12 @@ public static void main(String[] args) throws Exception {
             // ── Step 1: deploy ─────────────────────────────────────────────
             // Registers the workflow + task definitions on the server. Safe to
             // call repeatedly — re-deploying overwrites the previous version.
    -        List deployed = Agentspan.deploy(agent);
    +        List deployed = runtime.deploy(agent);
             System.out.println("Deployed: " + deployed);
     
             // ── Step 2: serve (on a daemon thread so main can keep going) ──
             Thread worker = new Thread(() -> {
    -            try { Agentspan.serve(agent); }
    +            try { runtime.serve(agent); }
                 catch (Throwable t) { /* serve() blocks; shutdown unblocks via InterruptedException */ }
             }, "example37-worker");
             worker.setDaemon(true);
    @@ -92,13 +93,13 @@ public static void main(String[] args) throws Exception {
             Thread.sleep(1_500);
     
             // ── Step 3: start an execution and wait for the result ─────────
    -        AgentHandle handle = Agentspan.start(agent, "Tell me about user U001.");
    +        AgentHandle handle = runtime.start(agent, "Tell me about user U001.");
             System.out.println("Started executionId=" + handle.getExecutionId());
             AgentResult result = handle.waitForResult();
             result.printResult();
     
             // ── Done ─────────────────────────────────────────────────────────
    -        Agentspan.shutdown();
    +        runtime.shutdown();
             System.out.println("OK — deploy + serve + run round-trip complete.");
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example38AgentspanGuardrails.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example38AgentspanGuardrails.java
    similarity index 87%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example38AgentspanGuardrails.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example38AgentspanGuardrails.java
    index 3a29204ba..e158d6223 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/adk/Example38AgentspanGuardrails.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/adk/Example38AgentspanGuardrails.java
    @@ -1,17 +1,17 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.adk;
    +package org.conductoross.conductor.ai.examples.adk;
     
    -import ai.agentspan.Agent;
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.enums.OnFail;
    -import ai.agentspan.enums.Position;
    -import ai.agentspan.examples.Settings;
    -import ai.agentspan.frameworks.AdkBridge;
    -import ai.agentspan.model.AgentResult;
    -import ai.agentspan.model.GuardrailDef;
    -import ai.agentspan.model.GuardrailResult;
    +import org.conductoross.conductor.ai.Agent;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.enums.OnFail;
    +import org.conductoross.conductor.ai.enums.Position;
    +import org.conductoross.conductor.ai.examples.Settings;
    +import org.conductoross.conductor.ai.frameworks.AdkBridge;
    +import org.conductoross.conductor.ai.model.AgentResult;
    +import org.conductoross.conductor.ai.model.GuardrailDef;
    +import org.conductoross.conductor.ai.model.GuardrailResult;
     
     import com.google.adk.agents.LlmAgent;
     
    @@ -73,6 +73,7 @@ private static GuardrailResult redactPii(String content) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             LlmAgent helper = LlmAgent.builder()
                     .name("contact_directory")
                     .description("Confirms contact details the user has just supplied.")
    @@ -100,12 +101,12 @@ public static void main(String[] args) {
                     .guardrails(piiRedaction)
                     .build();
     
    -        AgentResult result = Agentspan.run(guarded,
    +        AgentResult result = runtime.run(guarded,
                     "Please confirm the contact details I just sent: alice@example.com "
                     + "and phone 555-867-5309. Echo them back in your reply so I can "
                     + "double-check the spelling.");
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example01HelloWorld.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example01HelloWorld.java
    similarity index 82%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example01HelloWorld.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example01HelloWorld.java
    index 855ef66f0..5c84c5f8f 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example01HelloWorld.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example01HelloWorld.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -26,6 +26,7 @@
     public class Example01HelloWorld {
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -33,12 +34,12 @@ public static void main(String[] args) {
                     .modelName("gpt-4o-mini")
                     .build();
     
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                     model,
                     "Say hello and tell me a fun fact about Python programming."
             );
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example02ReactWithTools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example02ReactWithTools.java
    similarity index 95%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example02ReactWithTools.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example02ReactWithTools.java
    index d22dab874..a218f6160 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example02ReactWithTools.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example02ReactWithTools.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -167,6 +167,7 @@ private double parsePrimary() {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -177,7 +178,7 @@ public static void main(String[] args) {
             // Python's create_agent(llm, tools=[...]) sends no system prompt unless
             // the caller provides one — the drop-in overload defaults to no system
             // prompt, which matches.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "What is sqrt(256)? Also count words in 'the quick brown fox'. What is today's date?",
                 new UtilityTools()
    @@ -185,6 +186,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example03CustomTools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example03CustomTools.java
    similarity index 93%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example03CustomTools.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example03CustomTools.java
    index aaaee16c8..c6d964e5e 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example03CustomTools.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example03CustomTools.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -97,6 +97,7 @@ private static String trimNumber(double v) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -107,7 +108,7 @@ public static void main(String[] args) {
             // Python's create_agent(llm, tools=[...]) sends no system prompt unless
             // the caller provides one — the drop-in overload defaults to no system
             // prompt, which matches.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "Convert 100°C to Fahrenheit and Kelvin. Also format 1234567.891 with 2 decimal places.",
                 new CustomTools()
    @@ -115,6 +116,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example04StructuredOutput.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example04StructuredOutput.java
    similarity index 96%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example04StructuredOutput.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example04StructuredOutput.java
    index 0130a69a2..36a7d3a01 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example04StructuredOutput.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example04StructuredOutput.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -167,6 +167,7 @@ private static String escape(String s) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -176,7 +177,7 @@ public static void main(String[] args) {
     
             // The drop-in overload does not take a system prompt — fold the
             // instructions into the user message instead.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a book recommendation assistant. Use the recommend_book tool to find books.\n\n"
                     + "Recommend a great science fiction book and a good mystery novel.",
    @@ -185,6 +186,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example05PromptTemplates.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example05PromptTemplates.java
    similarity index 94%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example05PromptTemplates.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example05PromptTemplates.java
    index 5037d3204..4268b4808 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example05PromptTemplates.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example05PromptTemplates.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -91,6 +91,7 @@ public String suggestSynonyms(@P("word") String word) {
             + "Always use the available tools to look up definitions and synonyms before answering.";
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -100,7 +101,7 @@ public static void main(String[] args) {
     
             // The drop-in overload does not take a system prompt — fold the
             // persona into the user message instead.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 SYSTEM_PROMPT
                     + "\n\nWhat does 'serendipity' mean? And what are some synonyms for 'happy'?",
    @@ -109,6 +110,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example06ChatHistory.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example06ChatHistory.java
    similarity index 91%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example06ChatHistory.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example06ChatHistory.java
    index 3bb267d9f..1b264719a 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example06ChatHistory.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example06ChatHistory.java
    @@ -1,12 +1,12 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agent;
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    -import ai.agentspan.frameworks.LangChainBridge;
    +import org.conductoross.conductor.ai.Agent;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
    +import org.conductoross.conductor.ai.frameworks.LangChainBridge;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -72,6 +72,7 @@ public String recallFact(@P("topic") String topic) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -90,13 +91,13 @@ public static void main(String[] args) {
                 .stateful(true)
                 .build();
     
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 agent,
                 "Which planet in the solar system is farthest from the Sun?"
             );
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example07MemoryAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example07MemoryAgent.java
    similarity index 90%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example07MemoryAgent.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example07MemoryAgent.java
    index 384148a33..97c227224 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example07MemoryAgent.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example07MemoryAgent.java
    @@ -1,12 +1,12 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agent;
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    -import ai.agentspan.frameworks.LangChainBridge;
    +import org.conductoross.conductor.ai.Agent;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
    +import org.conductoross.conductor.ai.frameworks.LangChainBridge;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -69,6 +69,7 @@ public String getUserProfile(@P("username") String username) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             String instructions =
                 "You are a helpful HR assistant. Remember information from earlier in the conversation.";
     
    @@ -90,13 +91,13 @@ public static void main(String[] args) {
                 .stateful(true)
                 .build();
     
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 agent,
                 "Look up the profile for alice and tell me about her skills."
             );
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example08MultiToolAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example08MultiToolAgent.java
    similarity index 94%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example08MultiToolAgent.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example08MultiToolAgent.java
    index 538736b0e..16f320643 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example08MultiToolAgent.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example08MultiToolAgent.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -94,6 +94,7 @@ public String getNewsHeadline(@P("topic") String topic) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -102,7 +103,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a multi-domain assistant with access to weather, stock, and news information.\n\n"
                     + "What's the weather in Tokyo, the price of AAPL stock, and the latest technology headline?",
    @@ -111,6 +112,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example09MathCalculator.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example09MathCalculator.java
    similarity index 97%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example09MathCalculator.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example09MathCalculator.java
    index 8f96df8de..c4802ff51 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example09MathCalculator.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example09MathCalculator.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -234,6 +234,7 @@ private static String trimNum(double v) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -242,7 +243,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a precise math assistant. Always use tools to compute exact answers.\n\n"
                 + "What is (2 ** 8) + (15 * 7)? Convert 5 miles to kilometers. "
    @@ -252,6 +253,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example10WebSearchAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example10WebSearchAgent.java
    similarity index 95%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example10WebSearchAgent.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example10WebSearchAgent.java
    index 4de98f034..266d126d9 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example10WebSearchAgent.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example10WebSearchAgent.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -111,6 +111,7 @@ public String summarizeResults(@dev.langchain4j.agent.tool.P("text") String text
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -119,7 +120,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a research assistant. Use search and retrieval tools to answer questions thoroughly.\n\n"
                     + "Research the history of Python programming language and give me a brief summary.",
    @@ -128,6 +129,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example11CodeReviewAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example11CodeReviewAgent.java
    similarity index 96%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example11CodeReviewAgent.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example11CodeReviewAgent.java
    index 1796dd397..55b6207da 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example11CodeReviewAgent.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example11CodeReviewAgent.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -151,6 +151,7 @@ private static boolean isValidIdent(String s) {
                 + "    return 'adult'\n";
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -159,7 +160,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are an expert code reviewer. Analyze code thoroughly using the available tools. "
                 + "Report findings clearly and suggest improvements.\n\n"
    @@ -169,6 +170,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example12DocumentSummarizer.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example12DocumentSummarizer.java
    similarity index 95%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example12DocumentSummarizer.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example12DocumentSummarizer.java
    index 57349664d..673ad05f7 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example12DocumentSummarizer.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example12DocumentSummarizer.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -115,6 +115,7 @@ public String extractKeySentences(
                 + "computer scientists.\n";
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -123,7 +124,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a document analysis assistant. Use tools to analyze document structure, "
                 + "then synthesize a concise summary with key takeaways.\n\n"
    @@ -133,6 +134,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example13CustomerServiceAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example13CustomerServiceAgent.java
    similarity index 95%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example13CustomerServiceAgent.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example13CustomerServiceAgent.java
    index 10e7a0fff..7ce0835b0 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example13CustomerServiceAgent.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example13CustomerServiceAgent.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -89,6 +89,7 @@ public String createSupportTicket(
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -97,7 +98,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are Alex, a friendly and professional customer service agent for ShopEasy. "
                 + "Always greet the customer warmly. Use tools to look up orders and answer questions. "
    @@ -109,6 +110,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example14ResearchAssistant.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example14ResearchAssistant.java
    similarity index 95%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example14ResearchAssistant.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example14ResearchAssistant.java
    index f945c0e16..1a01b14da 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example14ResearchAssistant.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example14ResearchAssistant.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -104,6 +104,7 @@ public String getStatistics(@dev.langchain4j.agent.tool.P("domain") String domai
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -112,7 +113,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a thorough research assistant. When answering questions, "
                 + "search academic sources, recent news, and statistics to provide well-rounded answers. "
    @@ -124,6 +125,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example15DataAnalyst.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example15DataAnalyst.java
    similarity index 97%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example15DataAnalyst.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example15DataAnalyst.java
    index 636054afa..5fe32f89b 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example15DataAnalyst.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example15DataAnalyst.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -184,6 +184,7 @@ public String detectOutliers(
                 + "Super Widget,8,400.00,0.50";
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -192,7 +193,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a data analyst. Analyze the provided data using statistical tools "
                 + "and present your findings clearly with insights and recommendations.\n\n"
    @@ -203,6 +204,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example16ContentWriter.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example16ContentWriter.java
    similarity index 95%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example16ContentWriter.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example16ContentWriter.java
    index 79b48f40e..1de3b25ad 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example16ContentWriter.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example16ContentWriter.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -136,6 +136,7 @@ private static String titleCase(String s) {
                 + "for web development. If you want to learn Python programming, start with the basics.\n";
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -144,7 +145,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a professional content strategist and writer. "
                 + "Help users create clear, engaging, SEO-friendly content. "
    @@ -156,6 +157,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example17SqlAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example17SqlAgent.java
    similarity index 98%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example17SqlAgent.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example17SqlAgent.java
    index 827b73a58..75387a9a5 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example17SqlAgent.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example17SqlAgent.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -413,6 +413,7 @@ private static int compareValues(Object a, Object b, boolean desc) {
         // ── Main ─────────────────────────────────────────────────────────────────
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // Reference Arrays import so the simulated table init compiles cleanly.
             @SuppressWarnings("unused")
             List _unused = Arrays.asList("ref");
    @@ -425,7 +426,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a SQL assistant. Always inspect the schema first, then write and execute a SELECT query. "
                     + "Translate natural language questions into correct SQL.\n\n"
    @@ -435,6 +436,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example18EmailDrafter.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example18EmailDrafter.java
    similarity index 95%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example18EmailDrafter.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example18EmailDrafter.java
    index f0a306f9e..7b559b65e 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example18EmailDrafter.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example18EmailDrafter.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -103,6 +103,7 @@ public String formatEmailTemplate(
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -111,7 +112,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a professional email writing assistant. Help users draft clear, "
                     + "appropriate, and effective emails. Always check tone and suggest subject lines.\n\n"
    @@ -122,6 +123,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example19FactChecker.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example19FactChecker.java
    similarity index 95%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example19FactChecker.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example19FactChecker.java
    index 1c52c3299..40767337a 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example19FactChecker.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example19FactChecker.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -147,6 +147,7 @@ public String extractClaims(@dev.langchain4j.agent.tool.P("text") String text) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -155,7 +156,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a rigorous fact-checker. Extract claims from text and verify them. "
                     + "Be precise about what is true, false, or nuanced. Always cite sources when available.\n\n"
    @@ -166,6 +167,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example20TranslationAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example20TranslationAgent.java
    similarity index 96%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example20TranslationAgent.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example20TranslationAgent.java
    index 767bf7784..dfd25c0e0 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example20TranslationAgent.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example20TranslationAgent.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -151,6 +151,7 @@ public String getLanguageFacts(@dev.langchain4j.agent.tool.P("language") String
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -159,7 +160,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a multilingual translation assistant. Detect languages, provide translations, "
                     + "and share interesting linguistic context. Be accurate and culturally sensitive.\n\n"
    @@ -170,6 +171,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example21SentimentAnalysis.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example21SentimentAnalysis.java
    similarity index 96%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example21SentimentAnalysis.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example21SentimentAnalysis.java
    index ecaa30eee..c52344d5e 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example21SentimentAnalysis.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example21SentimentAnalysis.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -153,6 +153,7 @@ private static int countIntersect(Set a, Set b) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -161,7 +162,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a sentiment analysis assistant. Analyze text for sentiment and emotions, "
                     + "providing clear scores and insights. Use tools for accurate analysis.\n\n"
    @@ -171,6 +172,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example22ClassificationAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example22ClassificationAgent.java
    similarity index 96%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example22ClassificationAgent.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example22ClassificationAgent.java
    index 933c237ec..9da1e20a7 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example22ClassificationAgent.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example22ClassificationAgent.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -144,6 +144,7 @@ public String getCategoryExamples(@dev.langchain4j.agent.tool.P("category") Stri
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -152,7 +153,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a text classification assistant. Analyze text for topic and intent, "
                     + "provide confidence-scored categories, and explain your classifications.\n\n"
    @@ -163,6 +164,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example23RecommendationAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example23RecommendationAgent.java
    similarity index 96%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example23RecommendationAgent.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example23RecommendationAgent.java
    index 9a4ceb8e7..6e23d509e 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example23RecommendationAgent.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example23RecommendationAgent.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -179,6 +179,7 @@ private static Book findByTitle(String title) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -187,7 +188,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a personalized book recommendation assistant. Use tools to find, score, "
                     + "and explain book recommendations based on the user's preferences.\n\n"
    @@ -198,6 +199,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example24OutputParsers.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example24OutputParsers.java
    similarity index 96%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example24OutputParsers.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example24OutputParsers.java
    index 3076ad097..2854917ed 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example24OutputParsers.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example24OutputParsers.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -149,6 +149,7 @@ private static String escape(String s) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // Demonstrate the record-like value object so it isn't unused.
             @SuppressWarnings("unused")
             ExtractedFields example = new ExtractedFields("2025-03-15", "$249.99", "billing@example.com");
    @@ -162,7 +163,7 @@ public static void main(String[] args) {
     
             // Python uses the shorter prompt below — fold it into the user
             // message via the drop-in overload for parity.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a data extraction and formatting assistant. "
                     + "Use tools to retrieve, parse, and structure information clearly.\n\n"
    @@ -174,6 +175,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example25AdvancedOrchestration.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example25AdvancedOrchestration.java
    similarity index 97%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example25AdvancedOrchestration.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example25AdvancedOrchestration.java
    index c85df325f..f0e230f03 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example25AdvancedOrchestration.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example25AdvancedOrchestration.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -223,6 +223,7 @@ private static Map parseSimpleJsonNumbers(String json) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -231,7 +232,7 @@ public static void main(String[] args) {
                 .build();
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                 model,
                 "You are a senior business intelligence analyst. When given a research request, "
                     + "systematically gather company data, market trends, and compute relevant metrics. "
    @@ -244,6 +245,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example26AgentspanGuardrails.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example26AgentspanGuardrails.java
    similarity index 83%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example26AgentspanGuardrails.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example26AgentspanGuardrails.java
    index fd40639d9..940700bfe 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/Example26AgentspanGuardrails.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/Example26AgentspanGuardrails.java
    @@ -1,16 +1,16 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agent;
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.enums.OnFail;
    -import ai.agentspan.enums.Position;
    -import ai.agentspan.frameworks.LangChainBridge;
    -import ai.agentspan.model.AgentResult;
    -import ai.agentspan.model.GuardrailDef;
    -import ai.agentspan.model.GuardrailResult;
    +import org.conductoross.conductor.ai.Agent;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.enums.OnFail;
    +import org.conductoross.conductor.ai.enums.Position;
    +import org.conductoross.conductor.ai.frameworks.LangChainBridge;
    +import org.conductoross.conductor.ai.model.AgentResult;
    +import org.conductoross.conductor.ai.model.GuardrailDef;
    +import org.conductoross.conductor.ai.model.GuardrailResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -54,6 +54,7 @@ private static GuardrailResult redactPii(String content) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -78,11 +79,11 @@ public static void main(String[] args) {
                     .guardrails(piiRedaction)
                     .build();
     
    -        AgentResult result = Agentspan.run(guarded,
    +        AgentResult result = runtime.run(guarded,
                     "Please confirm: alice@example.com and 555-867-5309. "
                     + "Echo them back so I can double-check the spelling.");
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/ExampleCredentials.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/ExampleCredentials.java
    similarity index 90%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/ExampleCredentials.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/ExampleCredentials.java
    index 107cff913..ed593b4b7 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/ExampleCredentials.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/ExampleCredentials.java
    @@ -1,15 +1,15 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agent;
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.annotations.Tool;
    -import ai.agentspan.internal.ToolRegistry;
    -import ai.agentspan.model.AgentResult;
    -import ai.agentspan.model.ToolDef;
    -import ai.agentspan.frameworks.LangChainBridge;
    +import org.conductoross.conductor.ai.Agent;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.annotations.Tool;
    +import org.conductoross.conductor.ai.internal.ToolRegistry;
    +import org.conductoross.conductor.ai.model.AgentResult;
    +import org.conductoross.conductor.ai.model.ToolDef;
    +import org.conductoross.conductor.ai.frameworks.LangChainBridge;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -101,6 +101,7 @@ public String getWeather(String city) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -138,10 +139,10 @@ public static void main(String[] args) {
             System.out.println("Tools: " + fullAgent.getTools().size());
             fullAgent.getTools().forEach(t -> System.out.println("  - " + t.getName()));
     
    -        AgentResult result = Agentspan.run(fullAgent,
    +        AgentResult result = runtime.run(fullAgent,
                 "What is the weather in Paris, and what is 22°C in Fahrenheit?");
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/ExamplePipeline.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/ExamplePipeline.java
    similarity index 92%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/ExamplePipeline.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/ExamplePipeline.java
    index 7864889da..d597a32c5 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langchain/ExamplePipeline.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langchain/ExamplePipeline.java
    @@ -1,12 +1,12 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langchain;
    +package org.conductoross.conductor.ai.examples.langchain;
     
    -import ai.agentspan.Agent;
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    -import ai.agentspan.frameworks.LangChainBridge;
    +import org.conductoross.conductor.ai.Agent;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
    +import org.conductoross.conductor.ai.frameworks.LangChainBridge;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -79,6 +79,7 @@ public java.util.Map getSalesStats(
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -116,10 +117,10 @@ public static void main(String[] args) {
             System.out.println("Pipeline: " + pipeline.getName());
             System.out.println("Stages: " + pipeline.getAgents().size());
     
    -        AgentResult result = Agentspan.run(pipeline,
    +        AgentResult result = runtime.run(pipeline,
                 "Generate a product report for SKU 'WDGT-3000'.");
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example01HelloWorld.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example01HelloWorld.java
    similarity index 82%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example01HelloWorld.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example01HelloWorld.java
    index 074eb8797..e5597843f 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example01HelloWorld.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example01HelloWorld.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langgraph;
    +package org.conductoross.conductor.ai.examples.langgraph;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -22,6 +22,7 @@
     public class Example01HelloWorld {
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -31,12 +32,12 @@ public static void main(String[] args) {
     
             AgentExecutor.Builder agent = AgentExecutor.builder().chatModel(model);
     
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                     agent,
                     "Say hello and tell me a fun fact about state machines."
             );
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example02ReactWithTools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example02ReactWithTools.java
    similarity index 90%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example02ReactWithTools.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example02ReactWithTools.java
    index 7e0936e07..aeb392613 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example02ReactWithTools.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example02ReactWithTools.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langgraph;
    +package org.conductoross.conductor.ai.examples.langgraph;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -59,6 +59,7 @@ public String getToday() {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -70,7 +71,7 @@ public static void main(String[] args) {
             AgentExecutor.Builder agent = AgentExecutor.builder().chatModel(model);
             agent.toolsFromObject(tools);
     
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                     agent,
                     "What is 17 + 25? Also count words in 'the quick brown fox jumps'. "
                     + "And what is today's date?",
    @@ -79,6 +80,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example03Memory.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example03Memory.java
    similarity index 87%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example03Memory.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example03Memory.java
    index d79d6c4b3..4a41f81f3 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example03Memory.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example03Memory.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langgraph;
    +package org.conductoross.conductor.ai.examples.langgraph;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -32,6 +32,7 @@
     public class Example03Memory {
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -47,26 +48,26 @@ public static void main(String[] args) {
                     "You are a friendly assistant. Pay close attention to facts the user shares.\n\n";
     
             System.out.println("=== Turn 1: Introduce a name ===");
    -        AgentResult turn1 = Agentspan.run(
    +        AgentResult turn1 = runtime.run(
                     agent,
                     persona + "My name is Alice. Please remember that."
             );
             turn1.printResult();
     
             System.out.println("\n=== Turn 2: Ask the agent to recall ===");
    -        AgentResult turn2 = Agentspan.run(
    +        AgentResult turn2 = runtime.run(
                     agent,
                     persona + "Earlier I told you my name was Alice. What is my name?"
             );
             turn2.printResult();
     
             System.out.println("\n=== Turn 3: Continue the conversation ===");
    -        AgentResult turn3 = Agentspan.run(
    +        AgentResult turn3 = runtime.run(
                     agent,
                     persona + "Tell me one fun fact about the name Alice."
             );
             turn3.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example04SimpleStateGraph.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example04SimpleStateGraph.java
    similarity index 92%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example04SimpleStateGraph.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example04SimpleStateGraph.java
    index a72a60f9d..f7e4e3dd3 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example04SimpleStateGraph.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example04SimpleStateGraph.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langgraph;
    +package org.conductoross.conductor.ai.examples.langgraph;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -60,6 +60,7 @@ public String recordAnswer(@P("answer") String answer) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -72,7 +73,7 @@ public static void main(String[] args) {
             agent.toolsFromObject(tools);
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                     agent,
                     "You implement a three-stage query pipeline. "
                     + "ALWAYS call validate_query first, then refine_query, then "
    @@ -83,6 +84,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example05ToolNode.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example05ToolNode.java
    similarity index 92%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example05ToolNode.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example05ToolNode.java
    index ff3ec8d86..0213f7c16 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example05ToolNode.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example05ToolNode.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langgraph;
    +package org.conductoross.conductor.ai.examples.langgraph;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -67,6 +67,7 @@ public String lookupPopulation(@P("country") String country) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -79,7 +80,7 @@ public static void main(String[] args) {
             agent.toolsFromObject(tools);
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                     agent,
                     "You are a helpful geography assistant. Use the available tools to look up facts.\n\n"
                     + "What is the capital and population of Japan and Brazil?",
    @@ -88,6 +89,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example06ConditionalRouting.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example06ConditionalRouting.java
    similarity index 93%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example06ConditionalRouting.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example06ConditionalRouting.java
    index 86915d664..883b6eadb 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example06ConditionalRouting.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example06ConditionalRouting.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langgraph;
    +package org.conductoross.conductor.ai.examples.langgraph;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -64,6 +64,7 @@ public String handleNeutral(@P("text") String text) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -76,7 +77,7 @@ public static void main(String[] args) {
             agent.toolsFromObject(tools);
     
             // Drop-in overload — fold the routing instructions into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                     agent,
                     "You are a sentiment-routing agent. For every input: "
                     + "1) call classify_sentiment, "
    @@ -89,6 +90,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example07SystemPrompt.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example07SystemPrompt.java
    similarity index 89%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example07SystemPrompt.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example07SystemPrompt.java
    index e2cb07ecd..0b74454d8 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example07SystemPrompt.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example07SystemPrompt.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langgraph;
    +package org.conductoross.conductor.ai.examples.langgraph;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.model.chat.ChatModel;
     import dev.langchain4j.model.openai.OpenAiChatModel;
    @@ -44,6 +44,7 @@ public class Example07SystemPrompt {
         );
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -54,7 +55,7 @@ public static void main(String[] args) {
             AgentExecutor.Builder agent = AgentExecutor.builder().chatModel(model);
     
             // Drop-in overload — fold the persona into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                     agent,
                     TUTOR_SYSTEM_PROMPT
                     + "\n\nI want to understand why 1 + 1 = 2. Can you just tell me?"
    @@ -62,6 +63,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example08StructuredOutput.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example08StructuredOutput.java
    similarity index 92%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example08StructuredOutput.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example08StructuredOutput.java
    index da34b1e53..a1d25be6f 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example08StructuredOutput.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example08StructuredOutput.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langgraph;
    +package org.conductoross.conductor.ai.examples.langgraph;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -56,6 +56,7 @@ private static String escape(String s) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -68,7 +69,7 @@ public static void main(String[] args) {
             agent.toolsFromObject(tools);
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                     agent,
                     "You are a structured movie reviewer. For any movie title the user mentions, "
                     + "call review_movie and return the JSON it produces VERBATIM. "
    @@ -79,6 +80,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example09MathAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example09MathAgent.java
    similarity index 92%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example09MathAgent.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example09MathAgent.java
    index 304492928..40619c8dc 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example09MathAgent.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example09MathAgent.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langgraph;
    +package org.conductoross.conductor.ai.examples.langgraph;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -74,6 +74,7 @@ public String factorial(@P("n") int n) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -86,7 +87,7 @@ public static void main(String[] args) {
             agent.toolsFromObject(tools);
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                     agent,
                     "You are a math agent. Use the provided tools to compute every "
                     + "arithmetic step rather than doing it in your head. Show the result.\n\n"
    @@ -96,6 +97,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example10ResearchAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example10ResearchAgent.java
    similarity index 95%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example10ResearchAgent.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example10ResearchAgent.java
    index 4eb130410..7719c66ce 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example10ResearchAgent.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example10ResearchAgent.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langgraph;
    +package org.conductoross.conductor.ai.examples.langgraph;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -105,6 +105,7 @@ public String citeSource(@P("claim") String claim, @P("source_type") String sour
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -117,7 +118,7 @@ public static void main(String[] args) {
             agent.toolsFromObject(tools);
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                     agent,
                     "You are a research assistant. For any research question: "
                     + "1) call search for relevant information, "
    @@ -130,6 +131,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example11CustomerSupport.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example11CustomerSupport.java
    similarity index 94%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example11CustomerSupport.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example11CustomerSupport.java
    index ff3a2df9a..c6da4889f 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/langgraph/Example11CustomerSupport.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/langgraph/Example11CustomerSupport.java
    @@ -1,10 +1,10 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.langgraph;
    +package org.conductoross.conductor.ai.examples.langgraph;
     
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import dev.langchain4j.agent.tool.P;
     import dev.langchain4j.agent.tool.Tool;
    @@ -75,6 +75,7 @@ public String handleGeneral(@P("user_message") String userMessage) {
         }
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             // apiKey is required by LangChain4j's builder but unused — Agentspan
             // runs the LLM call on the server with server-registered credentials.
             ChatModel model = OpenAiChatModel.builder()
    @@ -87,7 +88,7 @@ public static void main(String[] args) {
             agent.toolsFromObject(tools);
     
             // Drop-in overload — fold the system prompt into the user message.
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                     agent,
                     "You are a customer support router. For EVERY incoming message: "
                     + "1) call greet, "
    @@ -102,6 +103,6 @@ public static void main(String[] args) {
             System.out.println("Status: " + result.getStatus());
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example01BasicAgent.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example01BasicAgent.java
    similarity index 71%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example01BasicAgent.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example01BasicAgent.java
    index a72966eb7..084e32e7a 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example01BasicAgent.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example01BasicAgent.java
    @@ -1,14 +1,14 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.openai;
    +package org.conductoross.conductor.ai.examples.openai;
     
    -import ai.agentspan.examples.Settings;
    +import org.conductoross.conductor.ai.examples.Settings;
     
    -import ai.agentspan.Agent;
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.frameworks.OpenAIAgent;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.Agent;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.frameworks.OpenAIAgent;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     /**
      * Example OpenAi 01 — Basic Agent
    @@ -29,17 +29,18 @@
     public class Example01BasicAgent {
     
         public static void main(String[] args) {
    +        AgentRuntime runtime = new AgentRuntime();
             Agent agent = OpenAIAgent.builder()
                     .name("greeter")
                     .instructions("You are a friendly assistant. Keep your responses concise and helpful.")
                     .model(Settings.LLM_MODEL)
                     .build();
     
    -        AgentResult result = Agentspan.run(
    +        AgentResult result = runtime.run(
                     agent,
                     "Say hello and tell me a fun fact about the Python programming language.");
             result.printResult();
     
    -        Agentspan.shutdown();
    +        runtime.shutdown();
         }
     }
    diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example02FunctionTools.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example02FunctionTools.java
    similarity index 89%
    rename from sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example02FunctionTools.java
    rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example02FunctionTools.java
    index 4751a267f..70e81e60f 100644
    --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example02FunctionTools.java
    +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example02FunctionTools.java
    @@ -1,15 +1,15 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.examples.openai;
    +package org.conductoross.conductor.ai.examples.openai;
     
    -import ai.agentspan.examples.Settings;
    +import org.conductoross.conductor.ai.examples.Settings;
     
    -import ai.agentspan.Agent;
    -import ai.agentspan.Agentspan;
    -import ai.agentspan.annotations.Tool;
    -import ai.agentspan.frameworks.OpenAIAgent;
    -import ai.agentspan.model.AgentResult;
    +import org.conductoross.conductor.ai.Agent;
    +import org.conductoross.conductor.ai.AgentRuntime;
    +import org.conductoross.conductor.ai.annotations.Tool;
    +import org.conductoross.conductor.ai.frameworks.OpenAIAgent;
    +import org.conductoross.conductor.ai.model.AgentResult;
     
     import java.util.LinkedHashMap;
     import java.util.Map;
    @@ -26,7 +26,7 @@
      *
      * 

    Note on annotation choice: the Python example uses * {@code @function_tool}; in Java the {@code OpenAIAgent} factory accepts - * both {@code @ai.agentspan.annotations.Tool} and + * both {@code @org.conductoross.conductor.ai.annotations.Tool} and * {@code @dev.langchain4j.agent.tool.Tool}. We use the Agentspan annotation * here because LangChain4j is only a {@code compileOnly} SDK dependency. * @@ -101,6 +101,7 @@ public String lookupPopulation(String city) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); Agent agent = OpenAIAgent.builder() .name("multi_tool_agent") .instructions( @@ -110,12 +111,12 @@ public static void main(String[] args) { .tools(new WeatherTools()) .build(); - AgentResult result = Agentspan.run( + AgentResult result = runtime.run( agent, "What's the weather in San Francisco? Also, what's the population there " + "and what's the square root of that number (just the digits)?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example03StructuredOutput.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example03StructuredOutput.java similarity index 84% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example03StructuredOutput.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example03StructuredOutput.java index eb274aa82..46b5c742a 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example03StructuredOutput.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example03StructuredOutput.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.openai; +package org.conductoross.conductor.ai.examples.openai; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.frameworks.OpenAIAgent; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.frameworks.OpenAIAgent; +import org.conductoross.conductor.ai.model.AgentResult; import java.util.List; @@ -52,6 +52,7 @@ public record MovieRecommendation(String title, int year, String genre, String r public record MovieList(List recommendations, String theme) {} public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); Agent agent = OpenAIAgent.builder() .name("movie_recommender") .instructions( @@ -62,11 +63,11 @@ public static void main(String[] args) { .outputType("MovieList") .build(); - AgentResult result = Agentspan.run( + AgentResult result = runtime.run( agent, "Recommend 3 sci-fi movies that explore the concept of artificial intelligence."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example04Handoffs.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example04Handoffs.java similarity index 91% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example04Handoffs.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example04Handoffs.java index 709e83491..86c343b17 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example04Handoffs.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example04Handoffs.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.openai; +package org.conductoross.conductor.ai.examples.openai; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.frameworks.OpenAIAgent; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.frameworks.OpenAIAgent; +import org.conductoross.conductor.ai.model.AgentResult; import java.util.LinkedHashMap; import java.util.Map; @@ -67,6 +67,7 @@ public String getProductInfo(String product_name) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // ── Specialist agents ───────────────────────────────────────── Agent orderAgent = OpenAIAgent.builder() .name("order_specialist") @@ -109,11 +110,11 @@ public static void main(String[] args) { .handoffs(orderAgent, refundAgent, salesAgent) .build(); - AgentResult result = Agentspan.run( + AgentResult result = runtime.run( triage, "I'd like a refund for order ORD-002, the product arrived damaged."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example05Guardrails.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example05Guardrails.java similarity index 83% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example05Guardrails.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example05Guardrails.java index 64a61b530..1de4f247c 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example05Guardrails.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example05Guardrails.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.openai; +package org.conductoross.conductor.ai.examples.openai; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.frameworks.OpenAIAgent; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.frameworks.OpenAIAgent; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example OpenAi 05 — Guardrails @@ -62,6 +62,7 @@ public String transferFunds(String from_account, String to_account, double amoun } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); Agent agent = OpenAIAgent.builder() .name("banking_assistant") .instructions( @@ -72,9 +73,9 @@ public static void main(String[] args) { .build(); // This should pass guardrails (no PII, no forbidden phrases in response). - AgentResult result = Agentspan.run(agent, "What's the balance on account ACC-100?"); + AgentResult result = runtime.run(agent, "What's the balance on account ACC-100?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example06ModelSettings.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example06ModelSettings.java similarity index 83% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example06ModelSettings.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example06ModelSettings.java index ea08a4fe8..1cd3a5d94 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example06ModelSettings.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example06ModelSettings.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.openai; +package org.conductoross.conductor.ai.examples.openai; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.frameworks.OpenAIAgent; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.frameworks.OpenAIAgent; +import org.conductoross.conductor.ai.model.AgentResult; /** * Example OpenAi 06 — Model Settings @@ -37,6 +37,7 @@ public class Example06ModelSettings { public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Creative agent — high-temperature intent (0.9) per Python original. Agent creativeAgent = OpenAIAgent.builder() .name("creative_writer") @@ -56,17 +57,17 @@ public static void main(String[] args) { .build(); System.out.println("=== Creative Agent (temp=0.9) ==="); - AgentResult creative = Agentspan.run( + AgentResult creative = runtime.run( creativeAgent, "Write a two-sentence story about a robot learning to paint."); creative.printResult(); System.out.println("\n=== Precise Agent (temp=0.1) ==="); - AgentResult precise = Agentspan.run( + AgentResult precise = runtime.run( preciseAgent, "Review this Python code: `data = eval(user_input)`"); precise.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example07Streaming.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example07Streaming.java similarity index 81% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example07Streaming.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example07Streaming.java index 28e36bc34..6a670f629 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example07Streaming.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example07Streaming.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.openai; +package org.conductoross.conductor.ai.examples.openai; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.frameworks.OpenAIAgent; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.frameworks.OpenAIAgent; +import org.conductoross.conductor.ai.model.AgentResult; import java.util.LinkedHashMap; import java.util.Map; @@ -58,6 +58,7 @@ public String searchKnowledgeBase(String query) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); Agent agent = OpenAIAgent.builder() .name("support_agent") .instructions( @@ -67,9 +68,9 @@ public static void main(String[] args) { .tools(new KnowledgeBaseTools()) .build(); - AgentResult result = Agentspan.run(agent, "What's your return policy for electronics?"); + AgentResult result = runtime.run(agent, "What's your return policy for electronics?"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example08AgentAsTool.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example08AgentAsTool.java similarity index 91% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example08AgentAsTool.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example08AgentAsTool.java index 8de1e1e9a..b912c380b 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example08AgentAsTool.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example08AgentAsTool.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.openai; +package org.conductoross.conductor.ai.examples.openai; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.frameworks.OpenAIAgent; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.frameworks.OpenAIAgent; +import org.conductoross.conductor.ai.model.AgentResult; import java.util.Arrays; import java.util.HashSet; @@ -82,6 +82,7 @@ public String extractKeywords(String text) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); Agent sentimentAgent = OpenAIAgent.builder() .name("sentiment_analyzer") .instructions( @@ -112,13 +113,13 @@ public static void main(String[] args) { .handoffs(sentimentAgent, keywordAgent) .build(); - AgentResult result = Agentspan.run( + AgentResult result = runtime.run( manager, "Analyze this review: 'The new laptop is excellent! The display is amazing " + "and the battery life is wonderful. However, the keyboard feels terrible " + "and the trackpad is the worst I've used.'"); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example09DynamicInstructions.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example09DynamicInstructions.java similarity index 87% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example09DynamicInstructions.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example09DynamicInstructions.java index 1741127b2..139ee41b5 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example09DynamicInstructions.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example09DynamicInstructions.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.openai; +package org.conductoross.conductor.ai.examples.openai; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.frameworks.OpenAIAgent; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.frameworks.OpenAIAgent; +import org.conductoross.conductor.ai.model.AgentResult; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -86,6 +86,7 @@ static String getDynamicInstructions() { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); Agent agent = OpenAIAgent.builder() .name("personal_assistant") .instructions(getDynamicInstructions()) @@ -93,11 +94,11 @@ public static void main(String[] args) { .tools(new TodoTools()) .build(); - AgentResult result = Agentspan.run( + AgentResult result = runtime.run( agent, "Show me my todo list and add 'Prepare demo for Friday' as high priority."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example10MultiModel.java b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example10MultiModel.java similarity index 91% rename from sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example10MultiModel.java rename to sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example10MultiModel.java index a89a48f8f..ad08883a1 100644 --- a/sdk/java/examples/src/main/java/ai/agentspan/examples/openai/Example10MultiModel.java +++ b/sdk/java/examples/src/main/java/org/conductoross/conductor/ai/examples/openai/Example10MultiModel.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.examples.openai; +package org.conductoross.conductor.ai.examples.openai; -import ai.agentspan.examples.Settings; +import org.conductoross.conductor.ai.examples.Settings; -import ai.agentspan.Agent; -import ai.agentspan.Agentspan; -import ai.agentspan.annotations.Tool; -import ai.agentspan.frameworks.OpenAIAgent; -import ai.agentspan.model.AgentResult; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.frameworks.OpenAIAgent; +import org.conductoross.conductor.ai.model.AgentResult; import java.util.LinkedHashMap; import java.util.Map; @@ -83,6 +83,7 @@ public String generateCodeSample(String language, String topic) { } public static void main(String[] args) { + AgentRuntime runtime = new AgentRuntime(); // Knowledgeable model for doc lookups (secondary model). Agent docSpecialist = OpenAIAgent.builder() .name("doc_specialist") @@ -116,11 +117,11 @@ public static void main(String[] args) { .handoffs(docSpecialist, codeSpecialist) .build(); - AgentResult result = Agentspan.run( + AgentResult result = runtime.run( triage, "I need a Python code example for authenticating with the API."); result.printResult(); - Agentspan.shutdown(); + runtime.shutdown(); } } diff --git a/sdk/java/spring/build.gradle b/sdk/java/spring/build.gradle index 38ea1a55f..c91b07d62 100644 --- a/sdk/java/spring/build.gradle +++ b/sdk/java/spring/build.gradle @@ -3,7 +3,7 @@ plugins { id 'com.vanniktech.maven.publish' version '0.34.0' } -group = 'ai.agentspan' +group = 'org.conductoross.conductor' java { toolchain { @@ -17,11 +17,17 @@ repositories { ext { springBootVersion = '3.3.5' + conductorClientVersion = '5.0.1' } dependencies { + // Agentspan SDK (brings in conductor-client transitively) api project(':') + // Conductor Spring auto-configuration — wires ApiClient from conductor.* properties + // so callers don't need to hand-roll the client bean themselves. + api "org.conductoross:conductor-client-spring:${conductorClientVersion}" + compileOnly "org.springframework.boot:spring-boot-autoconfigure:${springBootVersion}" compileOnly "org.springframework.boot:spring-boot:${springBootVersion}" @@ -51,7 +57,7 @@ mavenPublishing { signAllPublications() } - coordinates('ai.agentspan', 'java-sdk-spring', project.version.toString()) + coordinates('org.conductoross.conductor', 'conductor-ai-sdk-spring', project.version.toString()) pom { name = 'Agentspan Java SDK Spring Boot Starter' diff --git a/sdk/java/spring/src/main/java/ai/agentspan/spring/AgentspanAutoConfiguration.java b/sdk/java/spring/src/main/java/ai/agentspan/spring/AgentspanAutoConfiguration.java deleted file mode 100644 index bcb5d2d2c..000000000 --- a/sdk/java/spring/src/main/java/ai/agentspan/spring/AgentspanAutoConfiguration.java +++ /dev/null @@ -1,31 +0,0 @@ -package ai.agentspan.spring; - -import ai.agentspan.AgentConfig; -import ai.agentspan.AgentRuntime; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; - -@AutoConfiguration -@EnableConfigurationProperties(AgentspanProperties.class) -public class AgentspanAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public AgentConfig agentspanConfig(AgentspanProperties props) { - return new AgentConfig( - props.getServerUrl(), - props.getAuthKey(), - props.getAuthSecret(), - props.getWorkerPollIntervalMs(), - props.getWorkerThreadCount() - ); - } - - @Bean - @ConditionalOnMissingBean - public AgentRuntime agentRuntime(AgentConfig agentConfig) { - return new AgentRuntime(agentConfig); - } -} diff --git a/sdk/java/spring/src/main/java/ai/agentspan/spring/AgentspanProperties.java b/sdk/java/spring/src/main/java/ai/agentspan/spring/AgentspanProperties.java deleted file mode 100644 index decc9e7e1..000000000 --- a/sdk/java/spring/src/main/java/ai/agentspan/spring/AgentspanProperties.java +++ /dev/null @@ -1,28 +0,0 @@ -package ai.agentspan.spring; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "agentspan") -public class AgentspanProperties { - - private String serverUrl = "http://localhost:6767"; - private String authKey; - private String authSecret; - private int workerPollIntervalMs = 100; - private int workerThreadCount = 1; - - public String getServerUrl() { return serverUrl; } - public void setServerUrl(String serverUrl) { this.serverUrl = serverUrl; } - - public String getAuthKey() { return authKey; } - public void setAuthKey(String authKey) { this.authKey = authKey; } - - public String getAuthSecret() { return authSecret; } - public void setAuthSecret(String authSecret) { this.authSecret = authSecret; } - - public int getWorkerPollIntervalMs() { return workerPollIntervalMs; } - public void setWorkerPollIntervalMs(int workerPollIntervalMs) { this.workerPollIntervalMs = workerPollIntervalMs; } - - public int getWorkerThreadCount() { return workerThreadCount; } - public void setWorkerThreadCount(int workerThreadCount) { this.workerThreadCount = workerThreadCount; } -} diff --git a/sdk/java/spring/src/main/java/org/conductoross/conductor/ai/spring/AgentAutoConfiguration.java b/sdk/java/spring/src/main/java/org/conductoross/conductor/ai/spring/AgentAutoConfiguration.java new file mode 100644 index 000000000..2e672ea12 --- /dev/null +++ b/sdk/java/spring/src/main/java/org/conductoross/conductor/ai/spring/AgentAutoConfiguration.java @@ -0,0 +1,56 @@ +package org.conductoross.conductor.ai.spring; + +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; + +import io.orkes.conductor.client.ApiClient; +import io.orkes.conductor.client.spring.OrkesConductorClientAutoConfiguration; + +/** + * Spring Boot auto-configuration for the Agentspan SDK. + * + *

    This configuration wires two beans from {@code agentspan.*} properties: + *

      + *
    • {@link AgentConfig} — worker-runner tuning (poll interval, thread count)
    • + *
    • {@link AgentRuntime} — the SDK entry point
    • + *
    + * + *

    The {@link ApiClient} (server URL + auth) is not created here. It + * comes from {@link OrkesConductorClientAutoConfiguration}, which is pulled in + * transitively via {@code conductor-client-spring} and reads {@code conductor.*} + * properties. Users configure connectivity once in that namespace: + *

    {@code
    + * conductor.root-uri=http://localhost:6767/api
    + * conductor.security.client.key-id=my-key       # optional
    + * conductor.security.client.secret=my-secret    # optional
    + * }
    + * + *

    All three beans are conditional — define your own to override any of them. + */ +@AutoConfiguration(after = OrkesConductorClientAutoConfiguration.class) +@EnableConfigurationProperties(AgentProperties.class) +public class AgentAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public AgentConfig agentspanConfig(AgentProperties props) { + return new AgentConfig(props.getWorkerPollIntervalMs(), props.getWorkerThreadCount()); + } + + @Bean + @ConditionalOnMissingBean + public AgentRuntime agentRuntime(ApiClient conductorClient, AgentConfig agentConfig) { + return new AgentRuntime(conductorClient, agentConfig); + } + + @Bean + @ConditionalOnMissingBean + public AgentCatalog agentCatalog(ApplicationContext context) { + return new AgentCatalog(context); + } +} diff --git a/sdk/java/spring/src/main/java/org/conductoross/conductor/ai/spring/AgentCatalog.java b/sdk/java/spring/src/main/java/org/conductoross/conductor/ai/spring/AgentCatalog.java new file mode 100644 index 000000000..42f2f384b --- /dev/null +++ b/sdk/java/spring/src/main/java/org/conductoross/conductor/ai/spring/AgentCatalog.java @@ -0,0 +1,130 @@ +package org.conductoross.conductor.ai.spring; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.annotations.AgentDef; +import org.springframework.context.ApplicationContext; +import org.springframework.util.ClassUtils; + +/** + * Catalog of all agents declared via + * {@link org.conductoross.conductor.ai.annotations.AgentDef @AgentDef} methods on + * Spring beans. + * + *

    Auto-configured by {@link AgentAutoConfiguration}; inject it anywhere to look + * up agents by name: + * + *

    {@code
    + * @Component
    + * class Crew {
    + *     @AgentDef(model = "openai/gpt-4o")
    + *     public String support() { return "You handle support tickets."; }
    + * }
    + *
    + * @Service
    + * class TicketService {
    + *     TicketService(AgentRuntime runtime, AgentCatalog agents) {
    + *         runtime.run(agents.get("support"), "My invoice is wrong");
    + *     }
    + * }
    + * }
    + * + *

    Beans are scanned lazily on first access (after the context is fully started), + * and only beans whose class declares {@code @AgentDef} methods are instantiated by + * the scan. Duplicate agent names across beans fail fast. + */ +public class AgentCatalog { + + private final ApplicationContext context; + private volatile Map agents; + + public AgentCatalog(ApplicationContext context) { + this.context = context; + } + + /** All agents declared on beans in the context. */ + public List all() { + return new ArrayList<>(scan().values()); + } + + /** Names of all declared agents. */ + public Set names() { + return scan().keySet(); + } + + /** + * Look up an agent by name. + * + * @throws IllegalArgumentException if no bean declares an agent with that name + */ + public Agent get(String name) { + Agent agent = scan().get(name); + if (agent == null) { + throw new IllegalArgumentException( + "No agent named '" + name + "' declared on any bean. Available: " + scan().keySet()); + } + return agent; + } + + /** Look up an agent by name, empty if absent. */ + public Optional find(String name) { + return Optional.ofNullable(scan().get(name)); + } + + private Map scan() { + Map local = agents; + if (local == null) { + synchronized (this) { + local = agents; + if (local == null) { + local = doScan(); + agents = local; + } + } + } + return local; + } + + private Map doScan() { + Map found = new LinkedHashMap<>(); + Map sourceBeans = new LinkedHashMap<>(); + for (String beanName : context.getBeanDefinitionNames()) { + Class type = context.getType(beanName, false); + if (type == null) continue; + // Resolve the user class behind a CGLIB proxy; AgentRegistry's + // hierarchy-walking discovery then finds annotations through the + // proxy subclass when resolving the bean itself. + if (!declaresAgentDefs(ClassUtils.getUserClass(type))) continue; + + for (Agent agent : Agent.fromInstance(context.getBean(beanName))) { + String previous = sourceBeans.putIfAbsent(agent.getName(), beanName); + if (previous != null) { + throw new IllegalStateException("Duplicate agent name '" + agent.getName() + "' declared by beans '" + + previous + "' and '" + beanName + "'"); + } + found.put(agent.getName(), agent); + } + } + return Collections.unmodifiableMap(found); + } + + /** True if the class (or any ancestor/interface) declares an @AgentDef method. */ + private static boolean declaresAgentDefs(Class type) { + if (type == null || type == Object.class) return false; + for (Method method : type.getDeclaredMethods()) { + if (method.isAnnotationPresent(AgentDef.class)) return true; + } + for (Class iface : type.getInterfaces()) { + if (declaresAgentDefs(iface)) return true; + } + return declaresAgentDefs(type.getSuperclass()); + } +} diff --git a/sdk/java/spring/src/main/java/org/conductoross/conductor/ai/spring/AgentProperties.java b/sdk/java/spring/src/main/java/org/conductoross/conductor/ai/spring/AgentProperties.java new file mode 100644 index 000000000..9c9688b05 --- /dev/null +++ b/sdk/java/spring/src/main/java/org/conductoross/conductor/ai/spring/AgentProperties.java @@ -0,0 +1,47 @@ +package org.conductoross.conductor.ai.spring; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Agentspan-specific tuning knobs for the Spring Boot auto-configuration. + * + *

    Server connectivity (URL, auth key/secret) is handled by the Conductor + * Java SDK's own Spring starter via {@code conductor.*} properties — see + * {@link io.orkes.conductor.client.spring.OrkesConductorClientAutoConfiguration}. + * Only the Agentspan worker-runner settings live here. + * + *

    {@code
    + * # application.properties
    + *
    + * # Conductor client (from conductor-client-spring):
    + * conductor.root-uri=http://localhost:6767/api
    + * conductor.security.client.key-id=my-key       # optional
    + * conductor.security.client.secret=my-secret    # optional
    + *
    + * # Agentspan worker tuning (this class):
    + * agentspan.worker-poll-interval-ms=100
    + * agentspan.worker-thread-count=1
    + * }
    + */ +@ConfigurationProperties(prefix = "agentspan") +public class AgentProperties { + + private int workerPollIntervalMs = 100; + private int workerThreadCount = 1; + + public int getWorkerPollIntervalMs() { + return workerPollIntervalMs; + } + + public void setWorkerPollIntervalMs(int workerPollIntervalMs) { + this.workerPollIntervalMs = workerPollIntervalMs; + } + + public int getWorkerThreadCount() { + return workerThreadCount; + } + + public void setWorkerThreadCount(int workerThreadCount) { + this.workerThreadCount = workerThreadCount; + } +} diff --git a/sdk/java/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sdk/java/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 803dfbc71..b88e71102 100644 --- a/sdk/java/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/sdk/java/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1 @@ -ai.agentspan.spring.AgentspanAutoConfiguration +org.conductoross.conductor.ai.spring.AgentAutoConfiguration diff --git a/sdk/java/spring/src/test/java/ai/agentspan/spring/AgentspanAutoConfigurationTest.java b/sdk/java/spring/src/test/java/ai/agentspan/spring/AgentspanAutoConfigurationTest.java deleted file mode 100644 index 34b6d8bf1..000000000 --- a/sdk/java/spring/src/test/java/ai/agentspan/spring/AgentspanAutoConfigurationTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package ai.agentspan.spring; - -import ai.agentspan.AgentConfig; -import ai.agentspan.AgentRuntime; -import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.junit.jupiter.api.Assertions.*; - -class AgentspanAutoConfigurationTest { - - private final ApplicationContextRunner runner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(AgentspanAutoConfiguration.class)); - - @Test - void registersAgentConfigAndRuntimeBeansWithDefaults() { - runner.run(ctx -> { - assertTrue(ctx.containsBean("agentspanConfig")); - assertTrue(ctx.containsBean("agentRuntime")); - - AgentConfig config = ctx.getBean(AgentConfig.class); - assertEquals("http://localhost:6767", config.getServerUrl()); - assertEquals(100, config.getWorkerPollIntervalMs()); - assertEquals(1, config.getWorkerThreadCount()); - assertNull(config.getAuthKey()); - assertNull(config.getAuthSecret()); - }); - } - - @Test - void respectsCustomProperties() { - runner - .withPropertyValues( - "agentspan.server-url=http://myserver:9090", - "agentspan.auth-key=mykey", - "agentspan.auth-secret=mysecret", - "agentspan.worker-thread-count=4", - "agentspan.worker-poll-interval-ms=250" - ) - .run(ctx -> { - AgentConfig config = ctx.getBean(AgentConfig.class); - assertEquals("http://myserver:9090", config.getServerUrl()); - assertEquals("mykey", config.getAuthKey()); - assertEquals("mysecret", config.getAuthSecret()); - assertEquals(4, config.getWorkerThreadCount()); - assertEquals(250, config.getWorkerPollIntervalMs()); - }); - } - - @Test - void doesNotOverrideUserDefinedAgentConfigBean() { - AgentConfig custom = new AgentConfig("http://custom:1234", null, null, 50, 2); - runner - .withBean(AgentConfig.class, () -> custom) - .run(ctx -> { - AgentConfig config = ctx.getBean(AgentConfig.class); - assertSame(custom, config); - assertEquals("http://custom:1234", config.getServerUrl()); - }); - } - - @Test - void doesNotOverrideUserDefinedAgentRuntimeBean() { - AgentConfig config = new AgentConfig("http://localhost:6767", null, null, 100, 1); - AgentRuntime customRuntime = new AgentRuntime(config); - runner - .withBean(AgentRuntime.class, () -> customRuntime) - .run(ctx -> { - AgentRuntime runtime = ctx.getBean(AgentRuntime.class); - assertSame(customRuntime, runtime); - customRuntime.shutdown(); - }); - } -} diff --git a/sdk/java/spring/src/test/java/org/conductoross/conductor/ai/spring/AgentAutoConfigurationTest.java b/sdk/java/spring/src/test/java/org/conductoross/conductor/ai/spring/AgentAutoConfigurationTest.java new file mode 100644 index 000000000..390b50167 --- /dev/null +++ b/sdk/java/spring/src/test/java/org/conductoross/conductor/ai/spring/AgentAutoConfigurationTest.java @@ -0,0 +1,85 @@ +package org.conductoross.conductor.ai.spring; + +import static org.junit.jupiter.api.Assertions.*; + +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.AgentRuntime; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import io.orkes.conductor.client.ApiClient; + +class AgentAutoConfigurationTest { + + /** + * Provide a minimal ApiClient so tests don't need a live conductor-client-spring + * auto-configuration (which would require a real server URL to be set). + * In production, OrkesConductorClientAutoConfiguration wires this from + * conductor.* properties. + */ + private static ApiClient stubApiClient() { + return AgentRuntime.client("http://localhost:6767"); + } + + private final ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AgentAutoConfiguration.class)) + .withBean(ApiClient.class, AgentAutoConfigurationTest::stubApiClient); + + @Test + void wiresConfigAndRuntimeWithDefaults() { + runner.run(ctx -> { + assertTrue(ctx.containsBean("agentspanConfig"), "AgentConfig bean must be present"); + assertTrue(ctx.containsBean("agentRuntime"), "AgentRuntime bean must be present"); + + // No agentspanConductorClient bean — ApiClient comes from conductor-client-spring. + assertFalse( + ctx.containsBean("agentspanConductorClient"), + "auto-config must NOT create its own ApiClient — " + + "that is OrkesConductorClientAutoConfiguration's job"); + + AgentConfig config = ctx.getBean(AgentConfig.class); + assertEquals(100, config.getWorkerPollIntervalMs(), "default poll interval"); + assertEquals(1, config.getWorkerThreadCount(), "default thread count"); + }); + } + + @Test + void respectsWorkerTuningProperties() { + runner.withPropertyValues("agentspan.worker-thread-count=4", "agentspan.worker-poll-interval-ms=250") + .run(ctx -> { + AgentConfig config = ctx.getBean(AgentConfig.class); + assertEquals(4, config.getWorkerThreadCount()); + assertEquals(250, config.getWorkerPollIntervalMs()); + }); + } + + @Test + void serverUrlPropertiesAreNotAccepted() { + // agentspan.server-url no longer exists — setting it must not cause an error + // (Spring ignores unknown properties by default) and must not affect the client. + runner.withPropertyValues("agentspan.server-url=http://ignored:9090") + .run(ctx -> assertFalse( + ctx.getStartupFailure() != null, "unknown property must not break context startup")); + } + + @Test + void doesNotOverrideUserDefinedAgentConfigBean() { + AgentConfig custom = new AgentConfig(50, 2); + runner.withBean(AgentConfig.class, () -> custom).run(ctx -> { + AgentConfig config = ctx.getBean(AgentConfig.class); + assertSame(custom, config, "@ConditionalOnMissingBean must yield to user-provided AgentConfig"); + assertEquals(50, config.getWorkerPollIntervalMs()); + }); + } + + @Test + void doesNotOverrideUserDefinedAgentRuntimeBean() { + AgentRuntime customRuntime = new AgentRuntime(stubApiClient(), new AgentConfig()); + runner.withBean(AgentRuntime.class, () -> customRuntime).run(ctx -> { + AgentRuntime runtime = ctx.getBean(AgentRuntime.class); + assertSame(customRuntime, runtime, "@ConditionalOnMissingBean must yield to user-provided AgentRuntime"); + customRuntime.shutdown(); + }); + } +} diff --git a/sdk/java/spring/src/test/java/org/conductoross/conductor/ai/spring/AgentCatalogTest.java b/sdk/java/spring/src/test/java/org/conductoross/conductor/ai/spring/AgentCatalogTest.java new file mode 100644 index 000000000..ea969f6b4 --- /dev/null +++ b/sdk/java/spring/src/test/java/org/conductoross/conductor/ai/spring/AgentCatalogTest.java @@ -0,0 +1,107 @@ +package org.conductoross.conductor.ai.spring; + +import static org.junit.jupiter.api.Assertions.*; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.AgentRuntime; +import org.conductoross.conductor.ai.annotations.AgentDef; +import org.conductoross.conductor.ai.annotations.Tool; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import io.orkes.conductor.client.ApiClient; + +class AgentCatalogTest { + + private static ApiClient stubApiClient() { + return AgentRuntime.client("http://localhost:6767"); + } + + private final ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AgentAutoConfiguration.class)) + .withBean(ApiClient.class, AgentCatalogTest::stubApiClient); + + // ── Fixture beans ─────────────────────────────────────────────────── + + static class CrewBean { + @Tool(description = "Look up an order") + public String lookupOrder(String orderId) { + return "order " + orderId; + } + + @AgentDef(model = "openai/gpt-4o", instructions = "Handle billing questions.") + public void billing() {} + + @AgentDef(model = "openai/gpt-4o") + public String support() { + return "Handle support tickets."; + } + } + + static class DuplicateNameBean { + @AgentDef(model = "openai/gpt-4o", instructions = "Clashes with CrewBean.billing.") + public void billing() {} + } + + static class PlainBean {} + + // ── Tests ─────────────────────────────────────────────────────────── + + @Test + void collectsAgentsFromBeans() { + runner.withBean("crew", CrewBean.class) + .withBean("plain", PlainBean.class) + .run(ctx -> { + AgentCatalog catalog = ctx.getBean(AgentCatalog.class); + assertEquals(2, catalog.all().size()); + assertEquals(java.util.Set.of("billing", "support"), catalog.names()); + + Agent billing = catalog.get("billing"); + assertEquals("Handle billing questions.", billing.getInstructions()); + + Agent support = catalog.get("support"); + assertEquals("Handle support tickets.", support.getInstructions()); + // @Tool methods on the same bean attach automatically + assertEquals(1, support.getTools().size()); + assertEquals("lookupOrder", support.getTools().get(0).getName()); + }); + } + + @Test + void duplicateAgentNamesAcrossBeansFailFast() { + runner.withBean("crew", CrewBean.class) + .withBean("dup", DuplicateNameBean.class) + .run(ctx -> { + AgentCatalog catalog = ctx.getBean(AgentCatalog.class); + IllegalStateException e = assertThrows(IllegalStateException.class, catalog::all); + assertTrue(e.getMessage().contains("billing")); + assertTrue(e.getMessage().contains("crew")); + assertTrue(e.getMessage().contains("dup")); + }); + } + + @Test + void emptyCatalogWhenNoAgentBeans() { + runner.withBean("plain", PlainBean.class).run(ctx -> { + AgentCatalog catalog = ctx.getBean(AgentCatalog.class); + assertTrue(catalog.all().isEmpty()); + assertTrue(catalog.find("nope").isEmpty()); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> catalog.get("nope")); + assertTrue(e.getMessage().contains("nope")); + }); + } + + @Test + void userDefinedCatalogWins() { + runner.withBean(AgentCatalog.class, () -> new AgentCatalog(null) { + @Override + public java.util.List all() { + return java.util.List.of(); + } + }) + .run(ctx -> { + assertTrue(ctx.getBean(AgentCatalog.class).all().isEmpty()); + }); + } +} diff --git a/sdk/java/src/main/java/ai/agentspan/AgentConfig.java b/sdk/java/src/main/java/ai/agentspan/AgentConfig.java deleted file mode 100644 index 5bd95e690..000000000 --- a/sdk/java/src/main/java/ai/agentspan/AgentConfig.java +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan; - -/** - * Configuration for the Agentspan SDK. - * - *

    Use {@link #fromEnv()} to load configuration from environment variables, - * or construct directly with explicit values. - * - *

    Environment variables: - *

      - *
    • {@code AGENTSPAN_SERVER_URL} — server URL (default: http://localhost:6767)
    • - *
    • {@code AGENTSPAN_AUTH_KEY} — authentication key
    • - *
    • {@code AGENTSPAN_AUTH_SECRET} — authentication secret
    • - *
    • {@code AGENTSPAN_WORKER_POLL_INTERVAL} — worker poll interval in ms (default: 100)
    • - *
    • {@code AGENTSPAN_WORKER_THREADS} — worker thread count (default: 1)
    • - *
    - */ -public class AgentConfig { - private final String serverUrl; - private final String authKey; - private final String authSecret; - private final int workerPollIntervalMs; - private final int workerThreadCount; - - /** - * Create an AgentConfig with explicit values. - * - * @param serverUrl the Agentspan server URL - * @param authKey authentication key (may be null) - * @param authSecret authentication secret (may be null) - * @param workerPollIntervalMs worker poll interval in milliseconds - * @param workerThreadCount number of worker threads - */ - public AgentConfig( - String serverUrl, - String authKey, - String authSecret, - int workerPollIntervalMs, - int workerThreadCount) { - this.serverUrl = normalizeUrl(serverUrl != null ? serverUrl : "http://localhost:6767"); - this.authKey = authKey; - this.authSecret = authSecret; - this.workerPollIntervalMs = workerPollIntervalMs > 0 ? workerPollIntervalMs : 100; - this.workerThreadCount = workerThreadCount > 0 ? workerThreadCount : 1; - } - - /** - * Load configuration from environment variables with sensible defaults. - * - * @return a new AgentConfig - */ - public static AgentConfig fromEnv() { - return new AgentConfig( - env("AGENTSPAN_SERVER_URL", "http://localhost:6767"), - env("AGENTSPAN_AUTH_KEY", null), - env("AGENTSPAN_AUTH_SECRET", null), - Integer.parseInt(env("AGENTSPAN_WORKER_POLL_INTERVAL", "100")), - Integer.parseInt(env("AGENTSPAN_WORKER_THREADS", "1")) - ); - } - - private static String env(String key, String defaultValue) { - String val = System.getenv(key); - return val != null ? val : defaultValue; - } - - /** Strip trailing /api suffix so HttpApi can consistently prepend /api/... paths. */ - private static String normalizeUrl(String url) { - if (url == null) return null; - String stripped = url.stripTrailing(); - while (stripped.endsWith("/")) stripped = stripped.substring(0, stripped.length() - 1); - if (stripped.endsWith("/api")) stripped = stripped.substring(0, stripped.length() - 4); - while (stripped.endsWith("/")) stripped = stripped.substring(0, stripped.length() - 1); - return stripped; - } - - public String getServerUrl() { return serverUrl; } - public String getAuthKey() { return authKey; } - public String getAuthSecret() { return authSecret; } - public int getWorkerPollIntervalMs() { return workerPollIntervalMs; } - public int getWorkerThreadCount() { return workerThreadCount; } - - @Override - public String toString() { - return "AgentConfig{serverUrl=" + serverUrl + ", workerPollIntervalMs=" + workerPollIntervalMs - + ", workerThreadCount=" + workerThreadCount + "}"; - } -} diff --git a/sdk/java/src/main/java/ai/agentspan/Agentspan.java b/sdk/java/src/main/java/ai/agentspan/Agentspan.java deleted file mode 100644 index caf2fe104..000000000 --- a/sdk/java/src/main/java/ai/agentspan/Agentspan.java +++ /dev/null @@ -1,546 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan; - -import ai.agentspan.model.AgentHandle; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.AgentStream; -import ai.agentspan.model.DeploymentInfo; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; - -/** - * Static facade for the Agentspan SDK. - * - *

    Provides a convenient one-liner API using a shared singleton {@link AgentRuntime}. - * For full lifecycle control, use {@link AgentRuntime} directly. - * - *

    Example: - *

    {@code
    - * Agent agent = Agent.builder()
    - *     .name("assistant")
    - *     .model("openai/gpt-4o")
    - *     .build();
    - *
    - * AgentResult result = Agentspan.run(agent, "Hello!");
    - * result.printResult();
    - * Agentspan.shutdown();
    - * }
    - */ -public final class Agentspan { - private static final Logger logger = LoggerFactory.getLogger(Agentspan.class); - - private static volatile AgentRuntime defaultRuntime; - private static volatile AgentConfig defaultConfig; - private static final Object lock = new Object(); - - private Agentspan() {} - - /** - * Pre-configure the default singleton runtime. - * - *

    Must be called before the first {@link #run}, {@link #start}, or {@link #stream} call. - * - * @param config the configuration to use - * @throws IllegalStateException if the runtime is already initialized - */ - public static void configure(AgentConfig config) { - synchronized (lock) { - if (defaultRuntime != null) { - throw new IllegalStateException( - "configure() must be called before the first run/start/stream call. " - + "Call shutdown() first to reset the default runtime."); - } - defaultConfig = config; - } - } - - /** - * Execute an agent synchronously and return the result. - * - * @param agent the agent to run - * @param prompt the user's input message - * @return the agent result - */ - public static AgentResult run(Agent agent, String prompt) { - return getOrCreateRuntime().run(agent, prompt); - } - - /** - * Execute an agent asynchronously. - * - * @param agent the agent to run - * @param prompt the user's input message - * @return a CompletableFuture that resolves to the agent result - */ - public static CompletableFuture runAsync(Agent agent, String prompt) { - return getOrCreateRuntime().runAsync(agent, prompt); - } - - /** - * Start an agent (fire-and-forget) and return a handle. - * - * @param agent the agent to start - * @param prompt the user's input message - * @return a handle for monitoring and interacting with the agent - */ - public static AgentHandle start(Agent agent, String prompt) { - return getOrCreateRuntime().start(agent, prompt); - } - - /** - * Execute an agent and stream events as they occur. - * - * @param agent the agent to run - * @param prompt the user's input message - * @return an AgentStream for consuming events - */ - public static AgentStream stream(Agent agent, String prompt) { - return getOrCreateRuntime().stream(agent, prompt); - } - - /** - * Start an agent asynchronously and return a CompletableFuture for the handle. - * - * @param agent the agent to start - * @param prompt the user's input message - * @return a CompletableFuture that resolves to an AgentHandle - */ - public static CompletableFuture startAsync(Agent agent, String prompt) { - return getOrCreateRuntime().startAsync(agent, prompt); - } - - /** - * Stream agent events asynchronously. - * - * @param agent the agent to run - * @param prompt the user's input message - * @return a CompletableFuture resolving to an AgentStream - */ - public static CompletableFuture streamAsync(Agent agent, String prompt) { - return getOrCreateRuntime().streamAsync(agent, prompt); - } - - /** - * Compile an agent and return the server's plan without executing it. - * - * @param agent the agent to plan - * @return the plan response map from the server - */ - public static Map plan(Agent agent) { - return getOrCreateRuntime().plan(agent); - } - - /** - * Deploy agents to the server without executing them (CI/CD operation). - * - * @param agents one or more agents to deploy - * @return list of DeploymentInfo, one per deployed agent - */ - public static List deploy(Agent... agents) { - return getOrCreateRuntime().deploy(agents); - } - - /** - * Deploy agents to the server asynchronously. - * - * @param agents one or more agents to deploy - * @return CompletableFuture resolving to list of DeploymentInfo - */ - public static CompletableFuture> deployAsync(Agent... agents) { - return getOrCreateRuntime().deployAsync(agents); - } - - /** - * Re-attach to an existing agent execution and re-register workers. - * - * @param executionId the execution ID from a previous start() call - * @param agent the same Agent definition originally executed - * @return an AgentHandle for continued interaction - */ - public static AgentHandle resume(String executionId, Agent agent) { - return getOrCreateRuntime().resume(executionId, agent); - } - - /** - * Async version of {@link #resume}. - * - * @param executionId the execution ID - * @param agent the agent definition originally executed - * @return CompletableFuture resolving to an AgentHandle - */ - public static CompletableFuture resumeAsync(String executionId, Agent agent) { - return getOrCreateRuntime().resumeAsync(executionId, agent); - } - - /** - * Register workers and keep them polling until interrupted (blocking). - * - * @param agents agents whose workers should be served - */ - public static void serve(Agent... agents) { - getOrCreateRuntime().serve(agents); - } - - /** - * Shutdown the default singleton runtime, stopping all worker threads. - * - *

    Call this for explicit cleanup in long-running servers. In simple scripts, - * this is not necessary as workers are daemon threads. - */ - public static void shutdown() { - synchronized (lock) { - if (defaultRuntime != null) { - logger.info("Shutting down default Agentspan singleton runtime"); - defaultRuntime.shutdown(); - defaultRuntime = null; - } - } - } - - // ── Drop-in support for native framework agents (run / start / stream / - // deploy / serve / plan / resume all accept the raw native object) ── - - /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ - public static AgentResult run(Object agent, String prompt) { - return run(coerceAgent(agent), prompt); - } - - /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ - public static CompletableFuture runAsync(Object agent, String prompt) { - return runAsync(coerceAgent(agent), prompt); - } - - /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ - public static AgentHandle start(Object agent, String prompt) { - return start(coerceAgent(agent), prompt); - } - - /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ - public static CompletableFuture startAsync(Object agent, String prompt) { - return startAsync(coerceAgent(agent), prompt); - } - - /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ - public static AgentStream stream(Object agent, String prompt) { - return stream(coerceAgent(agent), prompt); - } - - /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ - public static CompletableFuture streamAsync(Object agent, String prompt) { - return streamAsync(coerceAgent(agent), prompt); - } - - /** Drop-in: accepts native ADK {@code BaseAgent} instances (or Agentspan {@link Agent}s). */ - public static List deploy(Object... agents) { - return deploy(coerceAgents(agents)); - } - - /** Drop-in: accepts native ADK {@code BaseAgent} instances (or Agentspan {@link Agent}s). */ - public static CompletableFuture> deployAsync(Object... agents) { - return deployAsync(coerceAgents(agents)); - } - - /** Drop-in: accepts native ADK {@code BaseAgent} instances (or Agentspan {@link Agent}s). */ - public static void serve(Object... agents) { - serve(coerceAgents(agents)); - } - - /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ - public static Map plan(Object agent) { - return plan(coerceAgent(agent)); - } - - /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ - public static AgentHandle resume(String executionId, Object agent) { - return resume(executionId, coerceAgent(agent)); - } - - /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ - public static CompletableFuture resumeAsync(String executionId, Object agent) { - return resumeAsync(executionId, coerceAgent(agent)); - } - - // ── LangChain4j / LangGraph4j drop-in overloads ───────────────────────── - // - // Same shape as the ADK drop-in: the user writes idiomatic - // LangChain4j (a ChatModel + @Tool POJOs) or LangGraph4j - // (AgentExecutor.Builder) and hands the native object straight to - // Agentspan.run / start / deploy / serve. No bridge call in user code. - // - // Why two flavours: - // - ChatModel + tools: pure LangChain4j idiom; no LangGraph - // dep needed. - // - AgentExecutor.Builder: LangGraph4j idiom; user passes the - // Builder along with the original tool - // POJOs (the Builder doesn't preserve - // references to them once toolsFromObject - // has run). - // - // Both delegate to LangChainBridge.agentBuilder under the hood. Users who - // need a custom agent name, system prompt structure, or Agentspan-side - // guardrails should call LangChainBridge.agentBuilder(...) directly. - - // 2-arg overloads exist so Java's overload resolution doesn't pick - // the more-general run(Object, String) over the varargs version when - // the user passes no tools. Without these, Agentspan.run(model, prompt) - // would silently dispatch through coerceAgent and fail with - // "Unsupported agent type: ChatModel". - - /** Drop-in: native LangChain4j {@code ChatModel}, no tools. */ - public static AgentResult run(dev.langchain4j.model.chat.ChatModel model, String prompt) { - return run(langchainAgent(model, null), prompt); - } - - /** Drop-in: native LangChain4j {@code ChatModel} + {@code @Tool} POJOs. */ - public static AgentResult run(dev.langchain4j.model.chat.ChatModel model, String prompt, Object... tools) { - return run(langchainAgent(model, tools), prompt); - } - - /** Drop-in: native LangChain4j {@code ChatModel}, no tools (async). */ - public static CompletableFuture runAsync(dev.langchain4j.model.chat.ChatModel model, String prompt) { - return runAsync(langchainAgent(model, null), prompt); - } - - /** Drop-in: native LangChain4j {@code ChatModel} + {@code @Tool} POJOs (async). */ - public static CompletableFuture runAsync(dev.langchain4j.model.chat.ChatModel model, String prompt, Object... tools) { - return runAsync(langchainAgent(model, tools), prompt); - } - - /** Drop-in: native LangChain4j {@code ChatModel}, no tools (start). */ - public static AgentHandle start(dev.langchain4j.model.chat.ChatModel model, String prompt) { - return start(langchainAgent(model, null), prompt); - } - - /** Drop-in: native LangChain4j {@code ChatModel} + {@code @Tool} POJOs (start). */ - public static AgentHandle start(dev.langchain4j.model.chat.ChatModel model, String prompt, Object... tools) { - return start(langchainAgent(model, tools), prompt); - } - - /** Drop-in: native LangChain4j {@code ChatModel}, no tools (stream). */ - public static AgentStream stream(dev.langchain4j.model.chat.ChatModel model, String prompt) { - return stream(langchainAgent(model, null), prompt); - } - - /** Drop-in: native LangChain4j {@code ChatModel} + {@code @Tool} POJOs (stream). */ - public static AgentStream stream(dev.langchain4j.model.chat.ChatModel model, String prompt, Object... tools) { - return stream(langchainAgent(model, tools), prompt); - } - - /** Drop-in deploy for a native LangChain4j configuration. */ - public static List deploy(dev.langchain4j.model.chat.ChatModel model, Object... tools) { - return deploy(langchainAgent(model, tools)); - } - - /** Drop-in serve for a native LangChain4j configuration. */ - public static void serve(dev.langchain4j.model.chat.ChatModel model, Object... tools) { - serve(langchainAgent(model, tools)); - } - - /** Drop-in: native LangGraph4j {@code AgentExecutor.Builder}, no tools. */ - public static AgentResult run(org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder builder, String prompt) { - return run(langgraphAgent(builder, null), prompt); - } - - /** Drop-in: native LangGraph4j {@code AgentExecutor.Builder} + tool POJOs. */ - public static AgentResult run(org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder builder, String prompt, Object... tools) { - return run(langgraphAgent(builder, tools), prompt); - } - - /** Drop-in: native LangGraph4j {@code AgentExecutor.Builder}, no tools (async). */ - public static CompletableFuture runAsync(org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder builder, String prompt) { - return runAsync(langgraphAgent(builder, null), prompt); - } - - /** Drop-in: native LangGraph4j {@code AgentExecutor.Builder} + tool POJOs (async). */ - public static CompletableFuture runAsync(org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder builder, String prompt, Object... tools) { - return runAsync(langgraphAgent(builder, tools), prompt); - } - - /** Drop-in: native LangGraph4j {@code AgentExecutor.Builder}, no tools (start). */ - public static AgentHandle start(org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder builder, String prompt) { - return start(langgraphAgent(builder, null), prompt); - } - - /** Drop-in: native LangGraph4j {@code AgentExecutor.Builder} + tool POJOs (start). */ - public static AgentHandle start(org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder builder, String prompt, Object... tools) { - return start(langgraphAgent(builder, tools), prompt); - } - - /** Drop-in: native LangGraph4j {@code AgentExecutor.Builder}, no tools (stream). */ - public static AgentStream stream(org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder builder, String prompt) { - return stream(langgraphAgent(builder, null), prompt); - } - - /** Drop-in: native LangGraph4j {@code AgentExecutor.Builder} + tool POJOs (stream). */ - public static AgentStream stream(org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder builder, String prompt, Object... tools) { - return stream(langgraphAgent(builder, tools), prompt); - } - - /** Drop-in deploy for a native LangGraph4j configuration. */ - public static List deploy(org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder builder, Object... tools) { - return deploy(langgraphAgent(builder, tools)); - } - - /** Drop-in serve for a native LangGraph4j configuration. */ - public static void serve(org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder builder, Object... tools) { - serve(langgraphAgent(builder, tools)); - } - - private static Agent langchainAgent(dev.langchain4j.model.chat.ChatModel model, Object[] tools) { - return ai.agentspan.frameworks.LangChainBridge - .agentBuilder("langchain_agent", model, null, tools == null ? new Object[0] : tools) - .build(); - } - - private static Agent langgraphAgent(org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder builder, Object[] tools) { - // The LangGraph4j AgentExecutor.Builder carries the ChatModel and the - // (optional) SystemMessage in package-private fields. We use reflection - // to recover them — failure here means a future ADK build changed the - // shape; we throw a clear message rather than silently degrading. - dev.langchain4j.model.chat.ChatModel model = readBuilderField( - builder, "chatModel", dev.langchain4j.model.chat.ChatModel.class); - if (model == null) { - throw new IllegalArgumentException( - "Agentspan.run(AgentExecutor.Builder, ...): the Builder has no chatModel set. " - + "Call .chatModel(...) before handing the Builder to Agentspan."); - } - String systemText = readSystemMessageText(builder); - - // Validate the Builder produces a compilable LangGraph4j StateGraph - // before shipping the config — same safety check the old bridge did. - try { - builder.build(); - } catch (Exception e) { - throw new RuntimeException( - "AgentExecutor.Builder is not a valid LangGraph4j configuration: " - + e.getMessage(), e); - } - return ai.agentspan.frameworks.LangChainBridge - .agentBuilder("langgraph_agent", model, systemText, tools == null ? new Object[0] : tools) - .build(); - } - - @SuppressWarnings("unchecked") - private static T readBuilderField(Object o, String fieldName, Class expected) { - try { - java.lang.reflect.Field f = o.getClass().getDeclaredField(fieldName); - f.setAccessible(true); - Object v = f.get(o); - return expected.isInstance(v) ? (T) v : null; - } catch (NoSuchFieldException nsf) { - return null; - } catch (Throwable t) { - throw new RuntimeException("AgentExecutor.Builder field '" + fieldName - + "' is no longer accessible — likely a LangGraph4j upgrade. " - + "Open an issue.", t); - } - } - - private static String readSystemMessageText(Object builder) { - try { - java.lang.reflect.Field f = builder.getClass().getDeclaredField("systemMessage"); - f.setAccessible(true); - Object sys = f.get(builder); - if (sys == null) return null; - // dev.langchain4j.data.message.SystemMessage has a public text() method - java.lang.reflect.Method m = sys.getClass().getMethod("text"); - Object t = m.invoke(sys); - return t instanceof String s && !s.isEmpty() ? s : null; - } catch (Throwable t) { - return null; - } - } - - /** - * Internal: coerce a user-provided agent object to an Agentspan {@link Agent}. - * - *

    Supports: - *

      - *
    • {@link Agent} — returned as-is
    • - *
    • {@code com.google.adk.agents.BaseAgent} — translated via - * {@link ai.agentspan.frameworks.AdkBridge} (ADK must be on the - * runtime classpath when this branch executes)
    • - *
    - */ - private static Agent coerceAgent(Object agent) { - if (agent == null) { - throw new IllegalArgumentException("agent is null"); - } - if (agent instanceof Agent a) { - return a; - } - if (isInstanceOf(agent, "com.google.adk.agents.BaseAgent")) { - // ADK is on the classpath (we just resolved BaseAgent), so loading - // AdkBridge here is safe — its direct ADK references will link. - return ai.agentspan.frameworks.AdkBridge.toAgentspan( - (com.google.adk.agents.BaseAgent) agent); - } - throw new IllegalArgumentException( - "Unsupported agent type: " + agent.getClass().getName() - + ". Expected ai.agentspan.Agent or a native ADK BaseAgent."); - } - - private static Agent[] coerceAgents(Object[] agents) { - if (agents == null) return new Agent[0]; - Agent[] out = new Agent[agents.length]; - for (int i = 0; i < agents.length; i++) { - try { - out[i] = coerceAgent(agents[i]); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("agents[" + i + "]: " + e.getMessage(), e); - } - } - return out; - } - - /** - * Walk the entire type hierarchy (superclasses, interfaces, and - * superinterfaces of both) looking for a type whose FQN matches. - * - *

    Used instead of {@code instanceof} so the dispatcher compiles and - * runs without ADK on the classpath — only callers actually passing - * native ADK objects trigger the JVM to load ADK classes. The recursive - * walk also handles interface-typed frameworks (where the target is an - * interface inherited via a superclass) — important for future bridge - * targets even though ADK's {@code BaseAgent} is itself a class today. - */ - private static boolean isInstanceOf(Object o, String fqn) { - return matchesType(o.getClass(), fqn); - } - - private static boolean matchesType(Class c, String fqn) { - if (c == null) return false; - if (fqn.equals(c.getName())) return true; - if (matchesType(c.getSuperclass(), fqn)) return true; - for (Class i : c.getInterfaces()) { - if (matchesType(i, fqn)) return true; - } - return false; - } - - private static AgentRuntime getOrCreateRuntime() { - if (defaultRuntime == null) { - synchronized (lock) { - if (defaultRuntime == null) { - AgentConfig config = defaultConfig != null ? defaultConfig : AgentConfig.fromEnv(); - defaultRuntime = new AgentRuntime(config); - logger.info("Created default Agentspan singleton runtime"); - - // Register shutdown hook - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - if (defaultRuntime != null) { - defaultRuntime.shutdown(); - } - }, "agentspan-shutdown")); - } - } - } - return defaultRuntime; - } -} diff --git a/sdk/java/src/main/java/ai/agentspan/ClaudeCode.java b/sdk/java/src/main/java/ai/agentspan/ClaudeCode.java deleted file mode 100644 index 56a775fe4..000000000 --- a/sdk/java/src/main/java/ai/agentspan/ClaudeCode.java +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan; - -/** - * Configuration for {@code Agent(model=ClaudeCode(...))} or the short string {@code "claude-code/opus"}. - * - *

    {@code
    - * Agent reviewer = Agent.builder()
    - *     .name("reviewer")
    - *     .model(new ClaudeCode("opus", ClaudeCode.PermissionMode.ACCEPT_EDITS).toModelString())
    - *     .instructions("Review code quality")
    - *     .build();
    - * }
    - */ -public class ClaudeCode { - - public enum PermissionMode { - DEFAULT("default"), - ACCEPT_EDITS("acceptEdits"), - PLAN("plan"), - BYPASS("bypassPermissions"); - - private final String value; - PermissionMode(String value) { this.value = value; } - public String getValue() { return value; } - } - - private final String modelName; - private final PermissionMode permissionMode; - - public ClaudeCode(String modelName) { - this(modelName, PermissionMode.ACCEPT_EDITS); - } - - public ClaudeCode(String modelName, PermissionMode permissionMode) { - this.modelName = modelName != null ? modelName : ""; - this.permissionMode = permissionMode != null ? permissionMode : PermissionMode.ACCEPT_EDITS; - } - - /** Convert to the model string format used by {@code Agent.builder().model(...)}. */ - public String toModelString() { - if (modelName == null || modelName.isEmpty()) return "claude-code"; - return "claude-code/" + modelName; - } - - public String getModelName() { return modelName; } - public PermissionMode getPermissionMode() { return permissionMode; } - - @Override - public String toString() { - return "ClaudeCode{model=" + toModelString() + ", mode=" + permissionMode.getValue() + "}"; - } -} diff --git a/sdk/java/src/main/java/ai/agentspan/Credentials.java b/sdk/java/src/main/java/ai/agentspan/Credentials.java deleted file mode 100644 index 744ea552d..000000000 --- a/sdk/java/src/main/java/ai/agentspan/Credentials.java +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan; - -import ai.agentspan.exceptions.CredentialNotFoundException; - -import java.util.Collections; -import java.util.Map; - -/** - * Thread-local accessor for resolved secrets — the only safe way to read - * declared credentials inside a {@code @Tool} method. - * - *

    Java is tier-1-only by design. {@code System.getenv()} returns an - * immutable map at JVM start, so AgentSpan cannot inject env vars at - * runtime the way Python's {@code os.environ}-mutation works. Tool code - * must read its declared credentials via this accessor:

    - * - *
    - *   {@literal @}Tool(credentials = {"OPENAI_API_KEY"})
    - *   public String chat(String prompt) {
    - *       String key = Credentials.get("OPENAI_API_KEY");
    - *       OpenAIClient client = new OpenAIClient(key);
    - *       ...
    - *   }
    - * 
    - * - *

    The worker framework calls {@link #setForCall} immediately before - * invoking the user's handler and {@link #clearForCall} in a {@code finally} - * block after the call returns. The accessor is thread-local — concurrent - * worker threads see independent secret contexts and cannot leak across - * each other.

    - * - *

    See {@code docs/design/secret-injection-contract.md} for the full - * cross-SDK contract. Java's design corresponds to Python's contextvars - * accessor and .NET's per-call IToolContext — all tier-1 explicit-key.

    - */ -public final class Credentials { - - private static final ThreadLocal> CURRENT = - new ThreadLocal<>(); - - private Credentials() { - // static-only utility - } - - /** - * Read a resolved secret value by name. - * - * @param name the credential name declared in {@code @Tool(credentials = {...})} - * @return the plaintext value - * @throws CredentialNotFoundException if no secret context is set - * (called outside a {@code @Tool} method) or the name was not - * declared / not resolved - */ - public static String get(String name) { - Map ctx = CURRENT.get(); - if (ctx == null) { - throw new CredentialNotFoundException( - "Credentials.get(\"" + name + "\") called outside a credential-aware " - + "@Tool method. Either the calling code isn't a worker, or " - + "the tool was invoked without going through WorkerManager."); - } - String value = ctx.get(name); - if (value == null) { - throw new CredentialNotFoundException(name); - } - return value; - } - - /** - * Read a resolved secret value, or {@code null} when the secret context - * isn't set / the name isn't present. Use this when you want to fall - * back gracefully instead of failing the tool. - */ - public static String getOrNull(String name) { - Map ctx = CURRENT.get(); - if (ctx == null) return null; - return ctx.get(name); - } - - /** - * Return a read-only view of the current call's resolved secrets. - * Empty map if no context is set. - */ - public static Map all() { - Map ctx = CURRENT.get(); - return ctx == null ? Collections.emptyMap() : Collections.unmodifiableMap(ctx); - } - - // ── Worker-framework hooks (not for application code) ──────────────── - - /** - * Establish the per-call secret context. Called by {@code WorkerManager} - * immediately before invoking a {@code @Tool} method. The handler runs - * in the same thread, so {@code ThreadLocal} reaches it. - */ - public static void setForCall(Map credentials) { - if (credentials == null || credentials.isEmpty()) { - CURRENT.remove(); - } else { - CURRENT.set(Map.copyOf(credentials)); - } - } - - /** - * Clear the per-call secret context. Called by {@code WorkerManager} in - * a {@code finally} block. Always safe to call even if no context was set. - */ - public static void clearForCall() { - CURRENT.remove(); - } -} diff --git a/sdk/java/src/main/java/ai/agentspan/UserProxyAgent.java b/sdk/java/src/main/java/ai/agentspan/UserProxyAgent.java deleted file mode 100644 index 15179c463..000000000 --- a/sdk/java/src/main/java/ai/agentspan/UserProxyAgent.java +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan; - -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * An agent that acts as a stand-in for a human user. - * - *

    When it is this agent's turn in a multi-agent conversation, the workflow pauses with a - * {@code HumanTask} and waits for real human input. The human's response becomes the agent's output. - * - *

    {@code
    - * Agent user = UserProxyAgent.create("human");
    - * Agent assistant = Agent.builder().name("assistant").model("openai/gpt-4o").build();
    - *
    - * Agent team = Agent.builder()
    - *     .name("chat")
    - *     .model("openai/gpt-4o")
    - *     .agents(user, assistant)
    - *     .strategy(Strategy.ROUND_ROBIN)
    - *     .maxTurns(6)
    - *     .build();
    - * }
    - */ -public class UserProxyAgent { - - /** Valid values for {@code humanInputMode}. */ - public static final String ALWAYS = "ALWAYS"; - public static final String TERMINATE = "TERMINATE"; - public static final String NEVER = "NEVER"; - - private UserProxyAgent() {} - - /** - * Create a UserProxyAgent. - * - * @param name agent name - * @param humanInputMode {@code "ALWAYS"}, {@code "TERMINATE"}, or {@code "NEVER"} - * @param defaultResponse response used when {@code humanInputMode="NEVER"} - * @param model LLM model (used as fallback when no human input is available) - */ - public static Agent create(String name, String humanInputMode, - String defaultResponse, String model) { - if (!ALWAYS.equals(humanInputMode) - && !TERMINATE.equals(humanInputMode) - && !NEVER.equals(humanInputMode)) { - throw new IllegalArgumentException( - "Invalid humanInputMode '" + humanInputMode + "'. Must be ALWAYS, TERMINATE, or NEVER"); - } - Map metadata = new LinkedHashMap<>(); - metadata.put("_agent_type", "user_proxy"); - metadata.put("_human_input_mode", humanInputMode); - metadata.put("_default_response", defaultResponse); - - return Agent.builder() - .name(name) - .model(model) - .instructions("You represent the human user in this conversation. " - + "Relay the human's input exactly as provided.") - .metadata(metadata) - .build(); - } -} diff --git a/sdk/java/src/main/java/ai/agentspan/execution/CliConfig.java b/sdk/java/src/main/java/ai/agentspan/execution/CliConfig.java deleted file mode 100644 index 30b28d77d..000000000 --- a/sdk/java/src/main/java/ai/agentspan/execution/CliConfig.java +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan.execution; - -import java.util.ArrayList; -import java.util.List; - -/** - * Configuration for first-class CLI command execution on an Agent. - * - *

    When set on an agent, a {@code run_command} worker tool is injected automatically, - * allowing the LLM to execute shell commands within the configured constraints. - * - *

    {@code
    - * Agent agent = Agent.builder()
    - *     .name("ops")
    - *     .model("openai/gpt-4o")
    - *     .cliConfig(new CliConfig.Builder()
    - *         .allowedCommands(List.of("git", "gh", "curl"))
    - *         .timeout(60)
    - *         .build())
    - *     .build();
    - * }
    - */ -public class CliConfig { - - private final boolean enabled; - private final List allowedCommands; - private final int timeout; - private final String workingDir; - private final boolean allowShell; - - private CliConfig(Builder builder) { - this.enabled = builder.enabled; - this.allowedCommands = builder.allowedCommands != null ? new ArrayList<>(builder.allowedCommands) : new ArrayList<>(); - this.timeout = builder.timeout > 0 ? builder.timeout : 30; - this.workingDir = builder.workingDir; - this.allowShell = builder.allowShell; - } - - public boolean isEnabled() { return enabled; } - public List getAllowedCommands() { return allowedCommands; } - public int getTimeout() { return timeout; } - public String getWorkingDir() { return workingDir; } - public boolean isAllowShell() { return allowShell; } - - public static Builder builder() { return new Builder(); } - - public static class Builder { - private boolean enabled = true; - private List allowedCommands; - private int timeout = 30; - private String workingDir; - private boolean allowShell = false; - - public Builder enabled(boolean enabled) { this.enabled = enabled; return this; } - public Builder allowedCommands(List allowedCommands) { this.allowedCommands = allowedCommands; return this; } - public Builder timeout(int timeout) { this.timeout = timeout; return this; } - public Builder workingDir(String workingDir) { this.workingDir = workingDir; return this; } - public Builder allowShell(boolean allowShell) { this.allowShell = allowShell; return this; } - - public CliConfig build() { return new CliConfig(this); } - } -} diff --git a/sdk/java/src/main/java/ai/agentspan/internal/HttpApi.java b/sdk/java/src/main/java/ai/agentspan/internal/HttpApi.java deleted file mode 100644 index 6c2040910..000000000 --- a/sdk/java/src/main/java/ai/agentspan/internal/HttpApi.java +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan.internal; - -import ai.agentspan.AgentConfig; -import ai.agentspan.exceptions.AgentAPIException; -import ai.agentspan.exceptions.AgentNotFoundException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.HashMap; -import java.util.Map; - -/** - * HTTP client for the Agent Runtime API. - * - *

    Mirrors the Python SDK's {@code AgentHttpClient}: one method per - * server endpoint, payload-shaped by the caller. Worker-protocol calls - * (poll / complete / fail / register) live in {@link WorkerHttp}. - */ -public class HttpApi { - private static final Logger logger = LoggerFactory.getLogger(HttpApi.class); - - private final AgentConfig config; - private final HttpClient httpClient; - - public HttpApi(AgentConfig config) { - this.config = config; - this.httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(30)) - .build(); - } - - /** {@code POST /api/agent/start} — start an agent execution. */ - public Map startAgent(Map payload) { - return post("/api/agent/start", payload); - } - - /** {@code POST /api/agent/compile} — compile agent config to workflow def. */ - public Map compileAgent(Map payload) { - return post("/api/agent/compile", payload); - } - - /** {@code POST /api/agent/deploy} — deploy (compile + register, no execution). */ - public Map deployAgent(Map payload) { - return post("/api/agent/deploy", payload); - } - - /** {@code GET /api/agent/{id}/status} — fetch execution status. */ - public Map getAgentStatus(String executionId) { - return get("/api/agent/" + executionId + "/status"); - } - - /** {@code POST /api/agent/{id}/respond} — respond to a waiting agent. */ - public void respond(String executionId, Map body) { - post("/api/agent/" + executionId + "/respond", body); - } - - /** {@code GET /api/workflow/{id}} — fetch raw workflow data (tasks, domain, run_id). */ - public Map getWorkflow(String executionId) { - return get("/api/workflow/" + executionId); - } - - // ── Internal helpers ───────────────────────────────────────────────── - - @SuppressWarnings("unchecked") - Map get(String path) { - try { - String url = config.getServerUrl() + path; - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofSeconds(30)) - .GET() - .header("Content-Type", "application/json"); - - addAuthHeaders(requestBuilder); - - HttpRequest request = requestBuilder.build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() >= 400) { - throw apiError(response.statusCode(), response.body()); - } - - if (response.body() == null || response.body().isEmpty()) { - return new HashMap<>(); - } - - return JsonMapper.fromJson(response.body(), Map.class); - } catch (AgentAPIException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("HTTP GET failed: " + path, e); - } - } - - private static AgentAPIException apiError(int statusCode, String body) { - if (statusCode == 404) { - return new AgentNotFoundException(statusCode, body); - } - return new AgentAPIException(statusCode, body); - } - - @SuppressWarnings("unchecked") - Map post(String path, Object body) { - try { - String url = config.getServerUrl() + path; - String jsonBody = JsonMapper.toJson(body); - - logger.debug("POST {} body: {}", url, jsonBody); - - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofSeconds(60)) - .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) - .header("Content-Type", "application/json"); - - addAuthHeaders(requestBuilder); - - HttpRequest request = requestBuilder.build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - logger.debug("POST {} -> {} {}", url, response.statusCode(), response.body()); - - if (response.statusCode() >= 400) { - throw apiError(response.statusCode(), response.body()); - } - - if (response.body() == null || response.body().isEmpty()) { - return new HashMap<>(); - } - - String responseBody = response.body().trim(); - if (responseBody.startsWith("{")) { - return JsonMapper.fromJson(responseBody, Map.class); - } else if (responseBody.startsWith("\"") || (!responseBody.startsWith("[") && !responseBody.startsWith("{"))) { - Map result = new HashMap<>(); - result.put("executionId", responseBody.replace("\"", "")); - return result; - } else { - return JsonMapper.fromJson(responseBody, Map.class); - } - } catch (AgentAPIException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("HTTP POST failed: " + path, e); - } - } - - void addAuthHeaders(HttpRequest.Builder builder) { - if (config.getAuthKey() != null && !config.getAuthKey().isEmpty()) { - builder.header("X-Auth-Key", config.getAuthKey()); - } - if (config.getAuthSecret() != null && !config.getAuthSecret().isEmpty()) { - builder.header("X-Auth-Secret", config.getAuthSecret()); - } - } - - AgentConfig getConfig() { - return config; - } - - HttpClient getRawClient() { - return httpClient; - } - - public HttpClient getHttpClient() { - return httpClient; - } -} diff --git a/sdk/java/src/main/java/ai/agentspan/internal/SseClient.java b/sdk/java/src/main/java/ai/agentspan/internal/SseClient.java deleted file mode 100644 index c0422ffd0..000000000 --- a/sdk/java/src/main/java/ai/agentspan/internal/SseClient.java +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan.internal; - -import ai.agentspan.AgentConfig; -import ai.agentspan.model.AgentEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Stream; - -/** - * Server-Sent Events (SSE) client for streaming agent events. - * - *

    Uses {@code java.net.http.HttpClient} (Java 11+) for SSE streaming. - * Events are placed into a {@code LinkedBlockingQueue} and consumed via - * {@link #nextEvent()}. - */ -public class SseClient implements AutoCloseable { - private static final Logger logger = LoggerFactory.getLogger(SseClient.class); - - /** Sentinel value to signal end-of-stream. */ - private static final AgentEvent DONE_SENTINEL = new AgentEvent(null, null, null, null, null, null, "", null, null); - - private final String url; - private final AgentConfig config; - private final HttpClient httpClient; - private final BlockingQueue eventQueue = new LinkedBlockingQueue<>(); - private final AtomicBoolean closed = new AtomicBoolean(false); - - public SseClient(String url, AgentConfig config, HttpClient httpClient) { - this.url = url; - this.config = config; - this.httpClient = httpClient; - } - - /** - * Connect and start receiving SSE events in a background thread. - */ - public void connect() { - Thread streamThread = new Thread(this::streamLoop, "agentspan-sse-" + url.hashCode()); - streamThread.setDaemon(true); - streamThread.start(); - } - - /** - * Block until the next event is available and return it. - * - * @return the next event, or null if the stream is done - */ - public AgentEvent nextEvent() { - try { - AgentEvent event = eventQueue.take(); - if (event == DONE_SENTINEL) { - return null; - } - return event; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return null; - } - } - - @Override - public void close() { - closed.set(true); - // Wake up any blocked nextEvent() calls - eventQueue.offer(DONE_SENTINEL); - } - - private void streamLoop() { - StringBuilder dataBuffer = new StringBuilder(); - String[] eventTypeHolder = {null}; - - try { - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofMinutes(10)) - .GET() - .header("Accept", "text/event-stream") - .header("Cache-Control", "no-cache"); - - if (config.getAuthKey() != null && !config.getAuthKey().isEmpty()) { - requestBuilder.header("X-Auth-Key", config.getAuthKey()); - } - if (config.getAuthSecret() != null && !config.getAuthSecret().isEmpty()) { - requestBuilder.header("X-Auth-Secret", config.getAuthSecret()); - } - - HttpRequest request = requestBuilder.build(); - HttpResponse> response = httpClient.send( - request, HttpResponse.BodyHandlers.ofLines()); - - if (response.statusCode() >= 400) { - logger.error("SSE connection failed with status {}", response.statusCode()); - eventQueue.offer(DONE_SENTINEL); - return; - } - - try { response.body().forEach(rawLine -> { - if (closed.get()) return; - - // Remove trailing \r if present - String line = rawLine.endsWith("\r") ? rawLine.substring(0, rawLine.length() - 1) : rawLine; - - if (line.isEmpty()) { - // Blank line: dispatch accumulated event - String data = dataBuffer.toString().trim(); - if (!data.isEmpty()) { - dispatchEvent(eventTypeHolder[0], data); - } - dataBuffer.setLength(0); - eventTypeHolder[0] = null; - return; - } - - if (line.startsWith(":")) { - // Comment / heartbeat — skip - return; - } - - if (line.startsWith("event:")) { - eventTypeHolder[0] = line.substring(6).trim(); - } else if (line.startsWith("id:")) { - // Last event ID — tracked but not used currently - } else if (line.startsWith("data:")) { - String dataChunk = line.substring(5); - if (dataChunk.startsWith(" ")) dataChunk = dataChunk.substring(1); - if (dataBuffer.length() > 0) dataBuffer.append("\n"); - dataBuffer.append(dataChunk); - } - }); } catch (java.io.UncheckedIOException ignored) { - // Stream closed while reading — expected on shutdown - } - - // Dispatch any remaining buffered data - String data = dataBuffer.toString().trim(); - if (!data.isEmpty()) { - dispatchEvent(eventTypeHolder[0], data); - } - - } catch (Exception e) { - if (!closed.get()) { - logger.error("SSE stream error: {}", e.getMessage(), e); - } - } finally { - eventQueue.offer(DONE_SENTINEL); - } - } - - @SuppressWarnings("unchecked") - private void dispatchEvent(String eventType, String data) { - try { - if ("[DONE]".equals(data)) { - eventQueue.offer(DONE_SENTINEL); - return; - } - - Map parsed = JsonMapper.fromJson(data, Map.class); - AgentEvent event = AgentEvent.fromMap(parsed); - - eventQueue.offer(event); - - // Stop after DONE event - if (event.getType() != null && "done".equals(event.getType().toJsonValue())) { - eventQueue.offer(DONE_SENTINEL); - } - } catch (Exception e) { - logger.warn("Failed to parse SSE event data: {} — {}", data, e.getMessage()); - } - } -} diff --git a/sdk/java/src/main/java/ai/agentspan/internal/WorkerCredentialFetcher.java b/sdk/java/src/main/java/ai/agentspan/internal/WorkerCredentialFetcher.java deleted file mode 100644 index dbf15656d..000000000 --- a/sdk/java/src/main/java/ai/agentspan/internal/WorkerCredentialFetcher.java +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan.internal; - -import ai.agentspan.AgentConfig; -import ai.agentspan.exceptions.CredentialAuthException; -import ai.agentspan.exceptions.CredentialNotFoundException; -import ai.agentspan.exceptions.CredentialRateLimitException; -import ai.agentspan.exceptions.CredentialServiceException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.URI; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Resolves declared secret values from the AgentSpan server using a worker - * execution token. Mirrors Python's {@code WorkerCredentialFetcher} and .NET's - * {@code AgentHttpClient.ResolveCredentialsAsync}. - * - *

    Java is tier-1-only per - * {@code docs/design/secret-injection-contract.md} §6 rule 1: {@code System.getenv()} - * is immutable at runtime, so env-injection isn't possible without reflection - * hacks. The fetcher returns values to the caller, who passes them to tool - * handlers via {@link ai.agentspan.Credentials}.

    - * - *

    Error contract — every failure mode produces a typed exception. Silent - * swallow (the bug class that affected .NET pre-fix) is structurally impossible - * here.

    - */ -public class WorkerCredentialFetcher { - - private static final Logger logger = LoggerFactory.getLogger(WorkerCredentialFetcher.class); - - private final AgentConfig config; - private final HttpApi httpApi; - - public WorkerCredentialFetcher(HttpApi httpApi) { - this.httpApi = httpApi; - this.config = httpApi.getConfig(); - } - - /** - * Resolve {@code names} via {@code POST /api/workers/secrets} using - * {@code executionToken}. - * - * @return name → plaintext value, with every requested name present - * (otherwise {@link CredentialNotFoundException} is thrown) - * @throws CredentialNotFoundException token absent, or server returned 200 - * with some names missing - * @throws CredentialAuthException token rejected (401) - * @throws CredentialRateLimitException 429 - * @throws CredentialServiceException 5xx or network failure - */ - public Map fetch(String executionToken, List names) { - if (names == null || names.isEmpty()) return Collections.emptyMap(); - - if (executionToken == null || executionToken.isBlank()) { - throw new CredentialNotFoundException(names); - } - - String url = config.getServerUrl() + "/api/workers/secrets"; - String body = JsonMapper.toJson(Map.of("token", executionToken, "names", names)); - - HttpRequest.Builder reqBuilder = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofSeconds(10)) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(body)); - httpApi.addAuthHeaders(reqBuilder); - - HttpResponse resp; - try { - resp = httpApi.getRawClient().send(reqBuilder.build(), - HttpResponse.BodyHandlers.ofString()); - } catch (Exception e) { - logger.error("Credential service unreachable: {}", e.toString()); - throw new CredentialServiceException(0, e.toString()); - } - - int status = resp.statusCode(); - if (status == 401) throw new CredentialAuthException(resp.body()); - if (status == 429) throw new CredentialRateLimitException(); - if (status >= 500) throw new CredentialServiceException(status, resp.body()); - if (status >= 400) throw new CredentialServiceException(status, resp.body()); - - @SuppressWarnings("unchecked") - Map resolved = (Map) - (Map) JsonMapper.fromJson(resp.body(), Map.class); - if (resolved == null) resolved = new LinkedHashMap<>(); - - List missing = new ArrayList<>(); - for (String name : names) { - if (!resolved.containsKey(name)) missing.add(name); - } - if (!missing.isEmpty()) { - logger.error("Credentials not found on server: {}", missing); - throw new CredentialNotFoundException(missing); - } - return resolved; - } -} diff --git a/sdk/java/src/main/java/ai/agentspan/internal/WorkerHttp.java b/sdk/java/src/main/java/ai/agentspan/internal/WorkerHttp.java deleted file mode 100644 index 70cf044b8..000000000 --- a/sdk/java/src/main/java/ai/agentspan/internal/WorkerHttp.java +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan.internal; - -import ai.agentspan.AgentConfig; -import ai.agentspan.exceptions.AgentAPIException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.URI; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Worker-protocol HTTP calls — poll, complete, fail, register task def. - * - *

    Kept separate from {@link HttpApi} (which is the user-facing agent - * API surface) so the two concerns don't bleed into each other. Only - * {@link WorkerManager} uses this class. - */ -public class WorkerHttp { - private static final Logger logger = LoggerFactory.getLogger(WorkerHttp.class); - - private final AgentConfig config; - private final HttpApi httpApi; - - public WorkerHttp(HttpApi httpApi) { - this.httpApi = httpApi; - this.config = httpApi.getConfig(); - } - - /** - * {@code GET /api/tasks/poll/{taskType}} — poll for a pending task. - * - *

    When {@code domain} is non-null the poll is scoped to that worker - * domain (the read-side complement of {@code startAgent(..., runId)}). - * - * @return task data, or {@code null} when no task is pending - */ - @SuppressWarnings("unchecked") - public Map pollTask(String taskType, String domain) { - try { - String url = config.getServerUrl() + "/api/tasks/poll/" + taskType; - if (domain != null && !domain.isEmpty()) { - url += "?domain=" + java.net.URLEncoder.encode(domain, java.nio.charset.StandardCharsets.UTF_8); - } - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofSeconds(10)) - .GET(); - - httpApi.addAuthHeaders(requestBuilder); - - HttpResponse response = httpApi.getRawClient().send( - requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() == 204 || response.body() == null || response.body().isEmpty()) { - return null; - } - - if (response.statusCode() == 200) { - return JsonMapper.fromJson(response.body(), Map.class); - } - - if (response.statusCode() >= 400) { - throw new AgentAPIException(response.statusCode(), response.body()); - } - - return null; - } catch (AgentAPIException e) { - throw e; - } catch (Exception e) { - logger.debug("Poll task failed for {}: {}", taskType, e.getMessage()); - return null; - } - } - - /** {@code POST /api/tasks} — report task completion. */ - public void completeTask(String taskId, String workflowInstanceId, Map output) { - Map body = new HashMap<>(); - body.put("taskId", taskId); - if (workflowInstanceId != null) body.put("workflowInstanceId", workflowInstanceId); - body.put("status", "COMPLETED"); - body.put("outputData", output); - httpApi.post("/api/tasks", body); - } - - /** {@code POST /api/tasks} — report task failure. */ - public void failTask(String taskId, String workflowInstanceId, String errorMessage) { - failTask(taskId, workflowInstanceId, errorMessage, false); - } - - /** - * Mark a task as terminally failed (non-retryable). Used for configuration - * problems like missing credentials — retrying won't fix them. - */ - public void failTaskTerminal(String taskId, String workflowInstanceId, String errorMessage) { - failTask(taskId, workflowInstanceId, errorMessage, true); - } - - private void failTask(String taskId, String workflowInstanceId, String errorMessage, - boolean terminal) { - Map body = new HashMap<>(); - body.put("taskId", taskId); - if (workflowInstanceId != null) body.put("workflowInstanceId", workflowInstanceId); - body.put("status", terminal ? "FAILED_WITH_TERMINAL_ERROR" : "FAILED"); - body.put("reasonForIncompletion", errorMessage); - try { - httpApi.post("/api/tasks", body); - } catch (Exception e) { - logger.warn("Failed to report task failure for {}: {}", taskId, e.getMessage()); - } - } - - /** {@code POST /api/metadata/taskdefs} — register a task definition. */ - public void registerTaskDef(String taskName) { - Map taskDef = new HashMap<>(); - taskDef.put("name", taskName); - taskDef.put("timeoutSeconds", 300); - taskDef.put("responseTimeoutSeconds", 300); - httpApi.post("/api/metadata/taskdefs", List.of(taskDef)); - } -} diff --git a/sdk/java/src/main/java/ai/agentspan/internal/WorkerManager.java b/sdk/java/src/main/java/ai/agentspan/internal/WorkerManager.java deleted file mode 100644 index 678513250..000000000 --- a/sdk/java/src/main/java/ai/agentspan/internal/WorkerManager.java +++ /dev/null @@ -1,343 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan.internal; - -import ai.agentspan.AgentConfig; -import ai.agentspan.Credentials; -import ai.agentspan.exceptions.CredentialAuthException; -import ai.agentspan.exceptions.CredentialNotFoundException; -import ai.agentspan.exceptions.CredentialRateLimitException; -import ai.agentspan.exceptions.CredentialServiceException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.reflect.Method; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -/** - * Manages worker threads that poll the server for pending tasks and execute tool functions. - */ -public class WorkerManager { - private static final Logger logger = LoggerFactory.getLogger(WorkerManager.class); - - /** Minimum Java version required for virtual threads. */ - private static final int VIRTUAL_THREAD_MIN_VERSION = 21; - - private final AgentConfig config; - private final WorkerHttp workerHttp; - private final WorkerCredentialFetcher credentialFetcher; - private final ConcurrentHashMap, Object>> handlers; - /** Optional worker domain per task name. Tasks without an entry poll the default queue. */ - private final ConcurrentHashMap taskDomains; - /** Declared credential names per task name. Empty list when no secrets are declared. */ - private final ConcurrentHashMap> taskCredentials; - /** - * Domain applied by the no-arg {@link #register(String, Function)} overload. - * AgentRuntime sets this for the lifetime of a single - * {@code prepareWorkers(agent, domain)} call so all subsequent register - * calls (callbacks, guardrails, gates, swarm-transfer, etc.) register - * under the same per-execution domain without having to thread the value - * through every call site. - */ - private volatile String currentDomain; - private ScheduledExecutorService scheduledExecutorService; - private final ConcurrentHashMap> workerFutures; - - private static final int JAVA_VERSION = detectJavaVersion(); - - public WorkerManager(AgentConfig config) { - this.config = config; - HttpApi httpApi = new HttpApi(config); - this.workerHttp = new WorkerHttp(httpApi); - this.credentialFetcher = new WorkerCredentialFetcher(httpApi); - this.handlers = new ConcurrentHashMap<>(); - this.taskDomains = new ConcurrentHashMap<>(); - this.taskCredentials = new ConcurrentHashMap<>(); - this.workerFutures = new ConcurrentHashMap<>(); - } - - /** - * Register a task handler function for the given task name. - * - * @param taskName the Conductor task type name - * @param handler the function to call when a task is polled - */ - public void register(String taskName, Function, Object> handler) { - register(taskName, handler, currentDomain); - } - - /** - * Set the domain that the no-arg {@link #register(String, Function)} - * overload will apply to subsequent calls. Pass {@code null} to clear. - * Used by {@code AgentRuntime.prepareWorkers(agent, domain)} so the many - * internal worker registrations in that method all pick up the run's - * domain without per-call wiring. - */ - public void setCurrentDomain(String domain) { - this.currentDomain = domain; - } - - /** Read the domain set by the most recent {@link #setCurrentDomain(String)}. */ - public String getCurrentDomain() { - return currentDomain; - } - - /** - * Register a task handler function scoped to a worker domain. - * - *

    When {@code domain} is non-null, the worker polls - * {@code /api/tasks/poll/{taskName}?domain={domain}} — only tasks routed - * to that domain (i.e. tasks belonging to the matching stateful run) are - * returned. This is the worker-side complement of the {@code runId} - * passed on {@code /api/agent/start}. - * - * @param taskName the Conductor task type name - * @param handler the function to call when a task is polled - * @param domain optional worker domain (per-stateful-run UUID), or null - */ - public void register( - String taskName, - Function, Object> handler, - String domain) { - register(taskName, handler, domain, Collections.emptyList()); - } - - /** - * Register a task handler scoped to a domain AND declaring a list of - * credential names that the worker should resolve from the server before - * invoking the handler. Resolved values are available to the handler via - * {@link ai.agentspan.Credentials#get(String)}. - */ - public void register( - String taskName, - Function, Object> handler, - String domain, - List credentials) { - boolean reRegister = handlers.containsKey(taskName); - handlers.put(taskName, handler); - if (domain != null && !domain.isEmpty()) { - taskDomains.put(taskName, domain); - } else { - taskDomains.remove(taskName); - } - if (credentials != null && !credentials.isEmpty()) { - taskCredentials.put(taskName, List.copyOf(credentials)); - } else { - taskCredentials.remove(taskName); - } - if (reRegister) { - logger.debug("Re-registering existing handler for task: {} (domain={})", taskName, domain); - return; - } - logger.info("Registered worker for task: {} (domain={})", taskName, domain); - - // Register task definition on the server - try { - workerHttp.registerTaskDef(taskName); - } catch (Exception e) { - logger.debug("Could not register task def {} (may already exist): {}", taskName, e.getMessage()); - } - - // If the executor is already running, start a polling thread immediately - // (workers registered after startAll() would otherwise never get polled) - if (scheduledExecutorService != null && !scheduledExecutorService.isShutdown()) { - startWorkerForTask(taskName); - } - } - - /** - * Start polling workers for all registered task types. - */ - public void startAll() { - if (scheduledExecutorService != null && !scheduledExecutorService.isShutdown()) { - return; // Already started - } - - scheduledExecutorService = Executors.newScheduledThreadPool(config.getWorkerThreadCount()); - - for (String taskName : handlers.keySet()) { - startWorkerForTask(taskName); - } - - logger.info("Started {} worker(s) for {} task(s)", - config.getWorkerThreadCount(), handlers.size()); - } - - private void startWorkerForTask(String taskName) { - ScheduledFuture future = scheduledExecutorService.scheduleAtFixedRate( - () -> pollAndExecute(taskName), - 0, - config.getWorkerPollIntervalMs(), - TimeUnit.MILLISECONDS - ); - workerFutures.put(taskName, future); - } - - /** - * Stop all polling workers. - */ - public void stop() { - if (scheduledExecutorService != null) { - scheduledExecutorService.shutdownNow(); - try { - if (!scheduledExecutorService.awaitTermination(5, TimeUnit.SECONDS)) { - logger.warn("Worker manager did not terminate cleanly"); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - logger.info("Worker manager stopped"); - } - } - - private void pollAndExecute(String taskName) { - Function, Object> handler = handlers.get(taskName); - if (handler == null) return; - - try { - String domain = taskDomains.get(taskName); - Map task = workerHttp.pollTask(taskName, domain); - if (task == null) return; - - String taskId = (String) task.get("taskId"); - if (taskId == null) { - taskId = (String) task.get("id"); - } - if (taskId == null) return; - - String workflowInstanceId = (String) task.get("workflowInstanceId"); - - @SuppressWarnings("unchecked") - Map inputData = (Map) task.getOrDefault("inputData", Map.of()); - - logger.debug("Executing task {} ({})", taskName, taskId); - - final String finalTaskId = taskId; - final String finalWorkflowId = workflowInstanceId; - executeTask(taskName, finalTaskId, finalWorkflowId, handler, inputData); - - } catch (Exception e) { - logger.error("Error in poll loop for task {}: {}", taskName, e.getMessage(), e); - } - } - - private void executeTask( - String taskName, - String taskId, - String workflowInstanceId, - Function, Object> handler, - Map inputData) { - - Runnable task = () -> { - // Resolve declared secrets BEFORE invoking the handler. Credential - // failures map to terminal task failures so Conductor doesn't burn - // retries on a configuration problem. See - // docs/design/secret-injection-contract.md — Java is tier-1-only - // (System.getenv is immutable; values reach the handler via the - // Secrets thread-local accessor, not via env mutation). - Map resolvedSecrets = Collections.emptyMap(); - List declared = taskCredentials.getOrDefault(taskName, Collections.emptyList()); - if (!declared.isEmpty()) { - String execToken = extractExecutionToken(inputData); - try { - resolvedSecrets = credentialFetcher.fetch(execToken, declared); - } catch (CredentialNotFoundException | CredentialAuthException - | CredentialRateLimitException | CredentialServiceException ce) { - logger.error("Credential resolution failed for task {} ({}): {}", - taskName, taskId, ce.getMessage()); - workerHttp.failTaskTerminal(taskId, workflowInstanceId, - "Credential resolution failed: " + ce.getMessage()); - return; - } - } - - try { - Credentials.setForCall(resolvedSecrets); - try { - Object result = handler.apply(inputData); - Map output = buildOutput(result); - workerHttp.completeTask(taskId, workflowInstanceId, output); - logger.debug("Completed task {} ({})", taskName, taskId); - } finally { - Credentials.clearForCall(); - } - } catch (Exception e) { - logger.error("Task {} ({}) failed: {}", taskName, taskId, e.getMessage(), e); - workerHttp.failTask(taskId, workflowInstanceId, e.getMessage()); - } - }; - - if (JAVA_VERSION >= VIRTUAL_THREAD_MIN_VERSION) { - runInVirtualThread(task); - } else { - // Run in the scheduled executor thread pool - task.run(); - } - } - - /** - * Use reflection to create a virtual thread (Java 21+) without requiring compile-time Java 21. - */ - private void runInVirtualThread(Runnable task) { - try { - Class threadClass = Thread.class; - Method ofVirtualMethod = threadClass.getMethod("ofVirtual"); - Object builderObj = ofVirtualMethod.invoke(null); - Method startMethod = builderObj.getClass().getMethod("start", Runnable.class); - startMethod.invoke(builderObj, task); - } catch (Exception e) { - // Fallback to regular thread if virtual thread creation fails - Thread t = new Thread(task, "agentspan-worker"); - t.setDaemon(true); - t.start(); - } - } - - /** - * Pull the execution token out of {@code inputData["__agentspan_ctx__"]["execution_token"]}. - * Server-side property name is snake-case (matches the .NET / TS extraction). - * Returns {@code null} if no token is present — caller treats that as - * "credential resolution will fail with NotFound". - */ - @SuppressWarnings("unchecked") - private static String extractExecutionToken(Map inputData) { - if (inputData == null) return null; - Object ctx = inputData.get("__agentspan_ctx__"); - if (!(ctx instanceof Map ctxMap)) return null; - Object token = ctxMap.get("execution_token"); - if (token == null) token = ctxMap.get("executionToken"); // tolerate camelCase - return token instanceof String s ? s : null; - } - - @SuppressWarnings("unchecked") - private Map buildOutput(Object result) { - if (result == null) return Map.of(); - if (result instanceof Map) return (Map) result; - return Map.of("result", result); - } - - private static int detectJavaVersion() { - String version = System.getProperty("java.version", "11"); - try { - if (version.startsWith("1.")) { - return Integer.parseInt(version.substring(2, 3)); - } - int dotIndex = version.indexOf('.'); - if (dotIndex > 0) { - return Integer.parseInt(version.substring(0, dotIndex)); - } - return Integer.parseInt(version); - } catch (NumberFormatException e) { - return 11; - } - } -} diff --git a/sdk/java/src/main/java/ai/agentspan/model/ToolContext.java b/sdk/java/src/main/java/ai/agentspan/model/ToolContext.java deleted file mode 100644 index 116e2040f..000000000 --- a/sdk/java/src/main/java/ai/agentspan/model/ToolContext.java +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan.model; - -import java.util.HashMap; -import java.util.Map; - -/** - * Context passed to tool functions during execution. - * - *

    {@link #getState()} provides a mutable dictionary that persists across all tool - * calls within the same agent execution. Tools can read and write to it to share - * data without relying on the LLM to relay state (mirrors Python SDK's - * {@code ToolContext.state}). - */ -public class ToolContext { - private final String sessionId; - private final String executionId; - private final String taskId; - private final Map state; - - public ToolContext(String sessionId, String executionId, String taskId) { - this(sessionId, executionId, taskId, new HashMap<>()); - } - - public ToolContext(String sessionId, String executionId, String taskId, Map initialState) { - this.sessionId = sessionId; - this.executionId = executionId; - this.taskId = taskId; - this.state = initialState != null ? new HashMap<>(initialState) : new HashMap<>(); - } - - public String getSessionId() { return sessionId; } - public String getExecutionId() { return executionId; } - public String getTaskId() { return taskId; } - - /** - * Shared state dictionary persisted across tool calls within the same agent execution. - * Mutate this map to pass data to subsequent tool calls. - */ - public Map getState() { return state; } -} diff --git a/sdk/java/src/main/java/ai/agentspan/schedule/ScheduleInfo.java b/sdk/java/src/main/java/ai/agentspan/schedule/ScheduleInfo.java deleted file mode 100644 index 834b5eb38..000000000 --- a/sdk/java/src/main/java/ai/agentspan/schedule/ScheduleInfo.java +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2026 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan.schedule; - -import java.util.Map; - -/** Server view of a schedule, returned by {@link Schedules#list(String)} / {@link Schedules#get(String)}. */ -public final class ScheduleInfo { - private final String name; - private final String shortName; - private final String agent; - private final String cron; - private final String timezone; - private final Map input; - private final boolean paused; - private final String pausedReason; - private final boolean catchup; - private final Long startAt; - private final Long endAt; - private final String description; - private final Long nextRun; - private final Long createTime; - private final Long updateTime; - private final String createdBy; - private final String updatedBy; - - public ScheduleInfo( - String name, String shortName, String agent, String cron, String timezone, - Map input, boolean paused, String pausedReason, boolean catchup, - Long startAt, Long endAt, String description, Long nextRun, - Long createTime, Long updateTime, String createdBy, String updatedBy) { - this.name = name; - this.shortName = shortName; - this.agent = agent; - this.cron = cron; - this.timezone = timezone; - this.input = input; - this.paused = paused; - this.pausedReason = pausedReason; - this.catchup = catchup; - this.startAt = startAt; - this.endAt = endAt; - this.description = description; - this.nextRun = nextRun; - this.createTime = createTime; - this.updateTime = updateTime; - this.createdBy = createdBy; - this.updatedBy = updatedBy; - } - - public String getName() { return name; } - public String getShortName() { return shortName; } - public String getAgent() { return agent; } - public String getCron() { return cron; } - public String getTimezone() { return timezone; } - public Map getInput() { return input; } - public boolean isPaused() { return paused; } - public String getPausedReason() { return pausedReason; } - public boolean isCatchup() { return catchup; } - public Long getStartAt() { return startAt; } - public Long getEndAt() { return endAt; } - public String getDescription() { return description; } - public Long getNextRun() { return nextRun; } - public Long getCreateTime() { return createTime; } - public Long getUpdateTime() { return updateTime; } - public String getCreatedBy() { return createdBy; } - public String getUpdatedBy() { return updatedBy; } - - @Override - public String toString() { - return "ScheduleInfo{name=" + name + ", agent=" + agent + ", cron=" + cron + ", paused=" + paused + "}"; - } -} diff --git a/sdk/java/src/main/java/ai/agentspan/Agent.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/Agent.java similarity index 66% rename from sdk/java/src/main/java/ai/agentspan/Agent.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/Agent.java index 6ec8937c6..ef8387b77 100644 --- a/sdk/java/src/main/java/ai/agentspan/Agent.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/Agent.java @@ -1,24 +1,27 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan; - -import ai.agentspan.enums.Strategy; -import ai.agentspan.execution.CliConfig; -import ai.agentspan.handoff.Handoff; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.PrefillToolCall; -import ai.agentspan.model.PromptTemplate; -import ai.agentspan.model.ToolDef; -import ai.agentspan.termination.TerminationCondition; +package org.conductoross.conductor.ai; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.Function; +import java.util.function.Supplier; import java.util.regex.Pattern; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.execution.CliConfig; +import org.conductoross.conductor.ai.handoff.Handoff; +import org.conductoross.conductor.ai.internal.AgentRegistry; +import org.conductoross.conductor.ai.model.ConversationMemory; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.PrefillToolCall; +import org.conductoross.conductor.ai.model.PromptTemplate; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.termination.TerminationCondition; + /** * An AI agent backed by a durable Conductor workflow. * @@ -42,7 +45,11 @@ public class Agent { private final String name; private final String model; - private final String instructions; + /** System prompt, held as a supplier so dynamic instructions are re-evaluated + * each time the config is serialized (every run submission) — matching the + * Python SDK, where callable instructions resolve at serialization time. */ + private final Supplier instructions; + private final List tools; private final List agents; private final Strategy strategy; @@ -54,7 +61,10 @@ public class Agent { private final int timeoutSeconds; private final TerminationCondition termination; private final Class outputType; + /** Session key for stateful/multi-turn agents. Sent on the /start payload, + * NOT included in the compiled agentConfig — it is an execution parameter. */ private final String sessionId; + private final List handoffs; private final Map> allowedTransitions; /** Plan-first preamble flag (Google ADK style). Renamed from @@ -62,6 +72,7 @@ public class Agent { * key for the PLAN_EXECUTE planner sub-agent slot. Wire-incompatible * with the old name. */ private final boolean enablePlanning; + private final boolean localCodeExecution; private final java.util.List allowedLanguages; private final int codeExecutionTimeout; @@ -82,22 +93,32 @@ public class Agent { * The server rejects the legacy ``agents=[planner, fallback]`` positional * shape with HTTP 400 once strategy is PLAN_EXECUTE — set these instead. */ private final Agent planner; + private final Agent fallback; /** PLAN_EXECUTE planner context — text snippets / URLs whose bodies are * fetched at planner-run time and appended to the planner prompt as a * ``## Reference Context`` block. Only meaningful with PLAN_EXECUTE; * the server compiler skips emission for any other strategy. */ - private final List plannerContext; + private final List plannerContext; + private final List prefillTools; private final boolean synthesize; private final boolean stateful; private final String baseUrl; - private final ai.agentspan.gate.TextGate gate; + private final org.conductoross.conductor.ai.gate.TextGate gate; private final Function, Map> beforeAgentCallback; private final Function, Map> afterAgentCallback; private final String framework; private final Map frameworkConfig; private final CliConfig cliConfig; + /** Conversation memory for stateful/multi-turn agents. Serialized as {@code memory}. */ + private final ConversationMemory memory; + /** OpenAI reasoning-model effort: {@code "low"}, {@code "medium"}, {@code "high"}. */ + private final String reasoningEffort; + /** Field names to redact in execution history and UI (Conductor {@code maskedFields}). */ + private final List maskedFields; + /** Token threshold for proactive context condensation. */ + private final Integer contextWindowBudget; private Agent(Builder builder) { this.name = builder.name; @@ -131,7 +152,8 @@ private Agent(Builder builder) { this.requiredTools = builder.requiredTools != null ? new ArrayList<>(builder.requiredTools) : new ArrayList<>(); this.credentials = builder.credentials != null ? new ArrayList<>(builder.credentials) : new ArrayList<>(); this.metadata = builder.metadata; - this.allowedCommands = builder.allowedCommands != null ? new ArrayList<>(builder.allowedCommands) : new ArrayList<>(); + this.allowedCommands = + builder.allowedCommands != null ? new ArrayList<>(builder.allowedCommands) : new ArrayList<>(); this.stopWhenTaskName = builder.stopWhenTaskName; this.fallbackMaxTurns = builder.fallbackMaxTurns; this.planner = builder.planner; @@ -140,12 +162,11 @@ private Agent(Builder builder) { // here so misconfig doesn't propagate to the server — same shape // as the planner/fallback validation in Python/TS SDKs. if (builder.plannerContext != null && !builder.plannerContext.isEmpty()) { - if (builder.strategy != ai.agentspan.enums.Strategy.PLAN_EXECUTE) { - throw new IllegalArgumentException( - "plannerContext is only valid with strategy=PLAN_EXECUTE. " - + "Got strategy=" + builder.strategy + ". The context block " - + "is appended to the planner's user prompt at runtime, " - + "which only exists in PLAN_EXECUTE."); + if (builder.strategy != org.conductoross.conductor.ai.enums.Strategy.PLAN_EXECUTE) { + throw new IllegalArgumentException("plannerContext is only valid with strategy=PLAN_EXECUTE. " + + "Got strategy=" + builder.strategy + ". The context block " + + "is appended to the planner's user prompt at runtime, " + + "which only exists in PLAN_EXECUTE."); } this.plannerContext = new ArrayList<>(builder.plannerContext); } else { @@ -161,6 +182,10 @@ private Agent(Builder builder) { this.framework = builder.framework; this.frameworkConfig = builder.frameworkConfig; this.cliConfig = builder.cliConfig; + this.memory = builder.memory; + this.reasoningEffort = builder.reasoningEffort; + this.maskedFields = builder.maskedFields != null ? new ArrayList<>(builder.maskedFields) : null; + this.contextWindowBudget = builder.contextWindowBudget; } /** @@ -181,7 +206,8 @@ public boolean isExternal() { */ public Agent then(Agent other) { List leftAgents = this.strategy == Strategy.SEQUENTIAL ? new ArrayList<>(this.agents) : List.of(this); - List rightAgents = other.strategy == Strategy.SEQUENTIAL ? new ArrayList<>(other.agents) : List.of(other); + List rightAgents = + other.strategy == Strategy.SEQUENTIAL ? new ArrayList<>(other.agents) : List.of(other); List allAgents = new ArrayList<>(leftAgents); allAgents.addAll(rightAgents); @@ -192,76 +218,262 @@ public Agent then(Agent other) { } return Agent.builder() - .name(combinedName.toString()) - .model(this.model) - .agents(allAgents) - .strategy(Strategy.SEQUENTIAL) - .build(); + .name(combinedName.toString()) + .model(this.model) + .agents(allAgents) + .strategy(Strategy.SEQUENTIAL) + .build(); } // ── Getters ────────────────────────────────────────────────────────── - public String getName() { return name; } - public String getModel() { return model; } - public String getInstructions() { return instructions; } - public List getTools() { return tools; } - public List getAgents() { return agents; } - public Strategy getStrategy() { return strategy; } - public Agent getRouter() { return router; } - public List getGuardrails() { return guardrails; } - public int getMaxTurns() { return maxTurns; } - public Integer getMaxTokens() { return maxTokens; } - public Double getTemperature() { return temperature; } - public int getTimeoutSeconds() { return timeoutSeconds; } - public TerminationCondition getTermination() { return termination; } - public Class getOutputType() { return outputType; } - public String getSessionId() { return sessionId; } - public List getHandoffs() { return handoffs; } - public Map> getAllowedTransitions() { return allowedTransitions; } - public boolean isEnablePlanning() { return enablePlanning; } - public boolean isLocalCodeExecution() { return localCodeExecution; } - public java.util.List getAllowedLanguages() { return allowedLanguages; } - public int getCodeExecutionTimeout() { return codeExecutionTimeout; } - public String getIncludeContents() { return includeContents; } - public Integer getThinkingBudgetTokens() { return thinkingBudgetTokens; } - public String getIntroduction() { return introduction; } - public PromptTemplate getInstructionsTemplate() { return instructionsTemplate; } - public Function, Map> getBeforeModelCallback() { return beforeModelCallback; } - public Function, Map> getAfterModelCallback() { return afterModelCallback; } - public List getCallbacks() { return callbacks; } - public List getRequiredTools() { return requiredTools; } - public List getCredentials() { return credentials; } - public Map getMetadata() { return metadata; } - public List getAllowedCommands() { return allowedCommands; } - public String getStopWhenTaskName() { return stopWhenTaskName; } - public Integer getFallbackMaxTurns() { return fallbackMaxTurns; } - public Agent getPlanner() { return planner; } - public Agent getFallback() { return fallback; } - public List getPlannerContext() { return plannerContext; } - public List getPrefillTools() { return prefillTools; } - public boolean isSynthesize() { return synthesize; } - public boolean isStateful() { return stateful; } - public String getBaseUrl() { return baseUrl; } - public ai.agentspan.gate.TextGate getGate() { return gate; } - public Function, Map> getBeforeAgentCallback() { return beforeAgentCallback; } - public Function, Map> getAfterAgentCallback() { return afterAgentCallback; } - public String getFramework() { return framework; } - public Map getFrameworkConfig() { return frameworkConfig; } - public CliConfig getCliConfig() { return cliConfig; } + public String getName() { + return name; + } + + public String getModel() { + return model; + } + + public String getInstructions() { + return instructions == null ? null : instructions.get(); + } + + public List getTools() { + return tools; + } + + public List getAgents() { + return agents; + } + + public Strategy getStrategy() { + return strategy; + } + + public Agent getRouter() { + return router; + } + + public List getGuardrails() { + return guardrails; + } + + public int getMaxTurns() { + return maxTurns; + } + + public Integer getMaxTokens() { + return maxTokens; + } + + public Double getTemperature() { + return temperature; + } + + public int getTimeoutSeconds() { + return timeoutSeconds; + } + + public TerminationCondition getTermination() { + return termination; + } + + public Class getOutputType() { + return outputType; + } + + public String getSessionId() { + return sessionId; + } + + public List getHandoffs() { + return handoffs; + } + + public Map> getAllowedTransitions() { + return allowedTransitions; + } + + public boolean isEnablePlanning() { + return enablePlanning; + } + + public boolean isLocalCodeExecution() { + return localCodeExecution; + } + + public java.util.List getAllowedLanguages() { + return allowedLanguages; + } + + public int getCodeExecutionTimeout() { + return codeExecutionTimeout; + } + + public String getIncludeContents() { + return includeContents; + } + + public Integer getThinkingBudgetTokens() { + return thinkingBudgetTokens; + } + + public String getIntroduction() { + return introduction; + } + + public PromptTemplate getInstructionsTemplate() { + return instructionsTemplate; + } + + public Function, Map> getBeforeModelCallback() { + return beforeModelCallback; + } + + public Function, Map> getAfterModelCallback() { + return afterModelCallback; + } + + public List getCallbacks() { + return callbacks; + } + + public List getRequiredTools() { + return requiredTools; + } + + public List getCredentials() { + return credentials; + } + + public Map getMetadata() { + return metadata; + } + + public List getAllowedCommands() { + return allowedCommands; + } + + public String getStopWhenTaskName() { + return stopWhenTaskName; + } + + public Integer getFallbackMaxTurns() { + return fallbackMaxTurns; + } + + public Agent getPlanner() { + return planner; + } + + public Agent getFallback() { + return fallback; + } + + public List getPlannerContext() { + return plannerContext; + } + + public List getPrefillTools() { + return prefillTools; + } + + public boolean isSynthesize() { + return synthesize; + } + + public boolean isStateful() { + return stateful; + } + + public String getBaseUrl() { + return baseUrl; + } + + public org.conductoross.conductor.ai.gate.TextGate getGate() { + return gate; + } + + public Function, Map> getBeforeAgentCallback() { + return beforeAgentCallback; + } + + public Function, Map> getAfterAgentCallback() { + return afterAgentCallback; + } + + public String getFramework() { + return framework; + } + + public Map getFrameworkConfig() { + return frameworkConfig; + } + + public CliConfig getCliConfig() { + return cliConfig; + } + + public ConversationMemory getMemory() { + return memory; + } + + public String getReasoningEffort() { + return reasoningEffort; + } + + public List getMaskedFields() { + return maskedFields; + } + + public Integer getContextWindowBudget() { + return contextWindowBudget; + } public static Builder builder() { return new Builder(); } + /** + * Resolve all {@link org.conductoross.conductor.ai.annotations.AgentDef @AgentDef}-annotated + * methods on an object into Agent instances. + * + *

    {@link org.conductoross.conductor.ai.annotations.Tool @Tool} and + * {@link org.conductoross.conductor.ai.annotations.GuardrailDef @GuardrailDef} methods + * on the same object are attached to each agent (all by default; filter with the + * annotation's {@code tools}/{@code guardrails} attributes). + * + * @param instance the object whose annotated methods define agents + * @return the resolved agents + */ + public static List fromInstance(Object instance) { + return AgentRegistry.fromInstance(instance); + } + + /** + * Resolve a single {@link org.conductoross.conductor.ai.annotations.AgentDef @AgentDef}-annotated + * method by agent name (the annotation {@code name}, or the method name if unset). + * + * @param instance the object whose annotated methods define agents + * @param name the agent name to resolve + * @return the resolved agent + * @throws IllegalArgumentException if no agent with that name is defined on the object + */ + public static Agent fromInstance(Object instance, String name) { + return AgentRegistry.fromInstance(instance, name); + } + @Override public String toString() { if (isExternal()) { return "Agent{name=" + name + ", external=true}"; } - StringBuilder sb = new StringBuilder("Agent{name=").append(name) - .append(", model=").append(model); + StringBuilder sb = + new StringBuilder("Agent{name=").append(name).append(", model=").append(model); if (!tools.isEmpty()) sb.append(", tools=").append(tools.size()); - if (!agents.isEmpty()) sb.append(", agents=").append(agents.size()).append(", strategy=").append(strategy); + if (!agents.isEmpty()) + sb.append(", agents=").append(agents.size()).append(", strategy=").append(strategy); sb.append("}"); return sb.toString(); } @@ -272,7 +484,7 @@ public String toString() { public static class Builder { private String name; private String model; - private String instructions; + private Supplier instructions; private List tools; private List agents; private Strategy strategy = Strategy.HANDOFF; @@ -306,17 +518,21 @@ public static class Builder { private Integer fallbackMaxTurns; private Agent planner; private Agent fallback; - private List plannerContext; + private List plannerContext; private List prefillTools; private boolean synthesize = true; private boolean stateful = false; private String baseUrl; - private ai.agentspan.gate.TextGate gate; + private org.conductoross.conductor.ai.gate.TextGate gate; private Function, Map> beforeAgentCallback; private Function, Map> afterAgentCallback; private String framework; private Map frameworkConfig; private CliConfig cliConfig; + private ConversationMemory memory; + private String reasoningEffort; + private List maskedFields; + private Integer contextWindowBudget; /** Set the agent name (required). Must match {@code ^[a-zA-Z_][a-zA-Z0-9_-]*$}. */ public Builder name(String name) { @@ -332,6 +548,18 @@ public Builder model(String model) { /** Set the system prompt / instructions for the agent. */ public Builder instructions(String instructions) { + this.instructions = instructions == null ? null : () -> instructions; + return this; + } + + /** + * Set dynamic instructions. The supplier is re-evaluated every time the + * agent config is serialized — i.e. on each run submission — so the prompt + * can reflect current state (date, feature flags, fetched context). + * Matches the Python SDK, where callable instructions resolve at + * serialization time. + */ + public Builder instructions(Supplier instructions) { this.instructions = instructions; return this; } @@ -432,16 +660,31 @@ public Builder sessionId(String sessionId) { * Restrict which agents can transfer to which other agents. * Keys are source agent names; values are lists of allowed target names. */ + /** + * Set SWARM strategy handoff triggers — rules that transfer control from this + * agent to another based on text mentions or tool results. + * + *

    Use {@link org.conductoross.conductor.ai.handoff.OnTextMention#of(String, String)}, + * {@link org.conductoross.conductor.ai.handoff.OnToolResult#of(String, String)}, or + * {@link org.conductoross.conductor.ai.handoff.OnCondition} to build entries. + */ public Builder handoffs(List handoffs) { this.handoffs = new ArrayList<>(handoffs); return this; } + /** Varargs convenience for {@link #handoffs(List)}. */ public Builder handoffs(Handoff... handoffs) { this.handoffs = new ArrayList<>(Arrays.asList(handoffs)); return this; } + /** + * Restrict agent-to-agent transfers in a SWARM — only the listed target agents + * are reachable from each source agent. + * Keys are source agent names; values are lists of allowed target names. + * Omit to allow unrestricted transfers. + */ public Builder allowedTransitions(Map> allowedTransitions) { this.allowedTransitions = allowedTransitions; return this; @@ -652,14 +895,14 @@ public Builder fallback(Agent fallback) { * block at runtime. URLs are fetched per planner invocation (no * compile-time fetch, no cache) so doc edits go live without recompile. * - *

    Pass {@link ai.agentspan.plans.Context} entries built via + *

    Pass {@link org.conductoross.conductor.ai.plans.Context} entries built via * {@code Context.text(...)} or {@code Context.url(...)} / * {@code Context.builder().url(...).header(...).build()} for credentialed * fetches — credential placeholders in the {@code ${CRED_NAME}} shape * are escaped server-side and resolved by the same credential pipeline * as HTTP tool headers. */ - public Builder plannerContext(List plannerContext) { + public Builder plannerContext(List plannerContext) { this.plannerContext = plannerContext; return this; } @@ -667,9 +910,9 @@ public Builder plannerContext(List plannerContext) { /** Shorthand: single-entry text-only planner context. Equivalent to * {@code plannerContext(List.of(Context.text(text)))}. */ public Builder plannerContext(String... texts) { - List ctx = new ArrayList<>(); + List ctx = new ArrayList<>(); for (String t : texts) { - ctx.add(ai.agentspan.plans.Context.text(t)); + ctx.add(org.conductoross.conductor.ai.plans.Context.text(t)); } this.plannerContext = ctx; return this; @@ -706,7 +949,7 @@ public Builder baseUrl(String baseUrl) { * Attach a gate to stop a sequential pipeline if this agent's output contains the sentinel text. * Only meaningful when the agent is part of a sequential pipeline ({@code agent.then(next)}). */ - public Builder gate(ai.agentspan.gate.TextGate gate) { + public Builder gate(org.conductoross.conductor.ai.gate.TextGate gate) { this.gate = gate; return this; } @@ -723,13 +966,29 @@ public Builder afterAgentCallback(Function, MapPrefer the bridge classes ({@link org.conductoross.conductor.ai.frameworks.OpenAIAgent}, + * {@link org.conductoross.conductor.ai.frameworks.AdkBridge}, etc.) — they set + * this field automatically. Direct use is for custom normalizers. + * + * @param framework wire value matching a server normalizer, e.g. {@code "openai"}, + * {@code "google_adk"}, {@code "langchain"}, {@code "skill"} + */ public Builder framework(String framework) { this.framework = framework; return this; } - /** Set the raw framework configuration map sent verbatim to the server. */ + /** + * Set framework-specific configuration merged into the {@code rawConfig} map + * sent to the server's normalizer. Only meaningful when {@link #framework(String)} + * is also set. The map is spread at the top level of the wire payload — e.g. + * {@code handoffs}, {@code output_type} for OpenAI; sub-agent definitions for ADK. + */ public Builder frameworkConfig(Map frameworkConfig) { this.frameworkConfig = frameworkConfig; return this; @@ -741,6 +1000,49 @@ public Builder cliConfig(CliConfig cliConfig) { return this; } + /** + * Attach conversation memory for stateful / multi-turn agents. + * Serialized to the server's {@code MemoryConfig} as {@code {messages, maxMessages}}. + */ + public Builder memory(ConversationMemory memory) { + this.memory = memory; + return this; + } + + /** + * Set the reasoning effort for OpenAI reasoning models (o-series, gpt-5-codex, etc.): + * {@code "low"}, {@code "medium"}, or {@code "high"}. Ignored by non-reasoning models. + */ + public Builder reasoningEffort(String reasoningEffort) { + this.reasoningEffort = reasoningEffort; + return this; + } + + /** + * Field names whose values are redacted in execution history and the UI + * (maps to Conductor's {@code WorkflowDef.maskedFields}). + */ + public Builder maskedFields(List maskedFields) { + this.maskedFields = maskedFields; + return this; + } + + /** Varargs convenience for {@link #maskedFields(List)}. */ + public Builder maskedFields(String... maskedFields) { + this.maskedFields = Arrays.asList(maskedFields); + return this; + } + + /** + * Token budget for proactive context condensation. When the estimated prompt + * token count exceeds this value, condensation fires — even below the model's + * actual context window. + */ + public Builder contextWindowBudget(int contextWindowBudget) { + this.contextWindowBudget = contextWindowBudget; + return this; + } + /** * Build the Agent. * @@ -752,8 +1054,8 @@ public Agent build() { } if (!VALID_NAME.matcher(name).matches()) { throw new IllegalArgumentException( - "Invalid agent name '" + name + "'. Must start with a letter or underscore " - + "and contain only letters, digits, underscores, or hyphens."); + "Invalid agent name '" + name + "'. Must start with a letter or underscore " + + "and contain only letters, digits, underscores, or hyphens."); } if (maxTurns < 1) { throw new IllegalArgumentException("maxTurns must be >= 1, got " + maxTurns); diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/AgentConfig.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/AgentConfig.java new file mode 100644 index 000000000..1535bb276 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/AgentConfig.java @@ -0,0 +1,67 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai; + +/** + * Worker-runner tuning for the Agentspan SDK. + * + *

    Connection details (server URL, auth key/secret) are NOT here — those are + * transport concerns owned by the Conductor client + * ({@code io.orkes.conductor.client.ApiClient}). Build one with + * {@link AgentRuntime#clientFromEnv()} / {@link AgentRuntime#client(String)} and + * pass it to {@link AgentRuntime}. This class carries only how the local worker + * runner behaves. + * + *

    Environment variables: + *

      + *
    • {@code AGENTSPAN_WORKER_POLL_INTERVAL} — worker poll interval in ms (default: 100)
    • + *
    • {@code AGENTSPAN_WORKER_THREADS} — worker thread count (default: 1)
    • + *
    + */ +public class AgentConfig { + private final int workerPollIntervalMs; + private final int workerThreadCount; + + /** + * Create worker tuning with explicit values. + * + * @param workerPollIntervalMs worker poll interval in milliseconds (≤0 → 100) + * @param workerThreadCount number of worker threads (≤0 → 1) + */ + public AgentConfig(int workerPollIntervalMs, int workerThreadCount) { + this.workerPollIntervalMs = workerPollIntervalMs > 0 ? workerPollIntervalMs : 100; + this.workerThreadCount = workerThreadCount > 0 ? workerThreadCount : 1; + } + + /** Default worker tuning (poll 100ms, 1 thread). */ + public AgentConfig() { + this(100, 1); + } + + /** Load worker tuning from environment variables with sensible defaults. */ + public static AgentConfig fromEnv() { + return new AgentConfig( + Integer.parseInt(env("AGENTSPAN_WORKER_POLL_INTERVAL", "100")), + Integer.parseInt(env("AGENTSPAN_WORKER_THREADS", "1"))); + } + + private static String env(String key, String defaultValue) { + String val = System.getenv(key); + return val != null ? val : defaultValue; + } + + public int getWorkerPollIntervalMs() { + return workerPollIntervalMs; + } + + public int getWorkerThreadCount() { + return workerThreadCount; + } + + @Override + public String toString() { + return "AgentConfig{workerPollIntervalMs=" + workerPollIntervalMs + ", workerThreadCount=" + workerThreadCount + + "}"; + } +} diff --git a/sdk/java/src/main/java/ai/agentspan/AgentRuntime.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/AgentRuntime.java similarity index 52% rename from sdk/java/src/main/java/ai/agentspan/AgentRuntime.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/AgentRuntime.java index 7a1e03586..3aabc2057 100644 --- a/sdk/java/src/main/java/ai/agentspan/AgentRuntime.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/AgentRuntime.java @@ -1,35 +1,57 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan; - -import ai.agentspan.internal.AgentConfigSerializer; -import ai.agentspan.internal.HttpApi; -import ai.agentspan.internal.SseClient; -import ai.agentspan.internal.WorkerManager; -import ai.agentspan.model.AgentHandle; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.AgentStream; -import ai.agentspan.model.ToolDef; -import ai.agentspan.schedule.Schedule; -import ai.agentspan.schedule.Schedules; -import ai.agentspan.skill.Skill; -import ai.agentspan.termination.AndTermination; -import ai.agentspan.termination.MaxMessageTermination; -import ai.agentspan.termination.OrTermination; -import ai.agentspan.termination.TerminationCondition; -import ai.agentspan.termination.TextMentionTermination; -import ai.agentspan.termination.TokenUsageTermination; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +package org.conductoross.conductor.ai; +import java.io.File; +import java.io.FileWriter; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.stream.Collectors; +import org.conductoross.conductor.ai.enums.Framework; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.execution.CliCommandExecutor; +import org.conductoross.conductor.ai.execution.CliConfig; +import org.conductoross.conductor.ai.internal.AgentClient; +import org.conductoross.conductor.ai.internal.AgentRequest; +import org.conductoross.conductor.ai.internal.SseClient; +import org.conductoross.conductor.ai.internal.StartResponse; +import org.conductoross.conductor.ai.internal.WorkerManager; +import org.conductoross.conductor.ai.model.AgentHandle; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.AgentStream; +import org.conductoross.conductor.ai.model.CompileResponse; +import org.conductoross.conductor.ai.model.DeploymentInfo; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.GuardrailResult; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.plans.Plan; +import org.conductoross.conductor.ai.schedule.Schedule; +import org.conductoross.conductor.ai.schedule.Schedules; +import org.conductoross.conductor.ai.skill.Skill; +import org.conductoross.conductor.ai.termination.AndTermination; +import org.conductoross.conductor.ai.termination.MaxMessageTermination; +import org.conductoross.conductor.ai.termination.OrTermination; +import org.conductoross.conductor.ai.termination.TerminationCondition; +import org.conductoross.conductor.ai.termination.TextMentionTermination; +import org.conductoross.conductor.ai.termination.TokenUsageTermination; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.netflix.conductor.client.http.WorkflowClient; + +import io.orkes.conductor.client.ApiClient; + /** * Main runtime for executing agents. * @@ -48,29 +70,103 @@ public class AgentRuntime implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(AgentRuntime.class); private final AgentConfig config; - private final HttpApi httpApi; + /** Single native Conductor client (ApiClient) for the whole runtime — shared by every + * typed client (AgentClient/WorkerManager/Schedules) and the SSE stream. */ + private final ApiClient conductorClient; + /** Agent control-plane client (/api/agent/*) built on the shared Conductor client. */ + private final AgentClient agentClient; + /** Standard Conductor workflow client for /api/workflow/* — used by AgentHandle to + * enrich results with token usage and tool calls after execution completes. */ + private final WorkflowClient workflowClient; + private final WorkerManager workerManager; - private final AgentConfigSerializer serializer; private volatile Schedules schedules; - /** - * Create a runtime using environment variable configuration. - */ + /** Create a runtime with a Conductor client and worker tuning both from environment. */ public AgentRuntime() { - this(AgentConfig.fromEnv()); + this(clientFromEnv(), AgentConfig.fromEnv()); + } + + /** Use the given worker tuning with a Conductor client built from environment. */ + public AgentRuntime(AgentConfig config) { + this(clientFromEnv(), config); + } + + /** Use the given Conductor client with worker tuning from environment. */ + public AgentRuntime(ApiClient conductorClient) { + this(conductorClient, AgentConfig.fromEnv()); } /** - * Create a runtime with explicit configuration. + * Create a runtime with an explicit Conductor client and worker tuning. + * + *

    The {@code conductorClient} (an {@code io.orkes.conductor.client.ApiClient}) + * owns the server URL and auth/token; build one with {@link #clientFromEnv()} or + * {@link #client(String)}. {@code config} carries only worker-runner tuning. * - * @param config the agent configuration + * @param conductorClient the native Conductor client (server URL + auth) + * @param config worker-runner tuning */ - public AgentRuntime(AgentConfig config) { + public AgentRuntime(ApiClient conductorClient, AgentConfig config) { this.config = config; - this.httpApi = new HttpApi(config); - this.workerManager = new WorkerManager(config); - this.serializer = new AgentConfigSerializer(); - logger.info("AgentRuntime initialized: {}", config.getServerUrl()); + this.conductorClient = conductorClient; + this.agentClient = new AgentClient(conductorClient); + this.workflowClient = new WorkflowClient(conductorClient); + this.workerManager = new WorkerManager(config, conductorClient); + logger.info("AgentRuntime initialized: {}", conductorClient.getBasePath()); + } + + // ── Conductor client factory ────────────────────────────────────────── + + /** + * Build a native Conductor client from environment + * ({@code AGENTSPAN_SERVER_URL}, {@code AGENTSPAN_AUTH_KEY}, + * {@code AGENTSPAN_AUTH_SECRET}). + */ + public static ApiClient clientFromEnv() { + return client( + envVar("AGENTSPAN_SERVER_URL", "http://localhost:6767"), + envVar("AGENTSPAN_AUTH_KEY", null), + envVar("AGENTSPAN_AUTH_SECRET", null)); + } + + /** Build an unauthenticated Conductor client for {@code serverUrl}. */ + public static ApiClient client(String serverUrl) { + return client(serverUrl, null, null); + } + + /** + * Build a Conductor client for {@code serverUrl} with optional key/secret auth + * (the SDK's native token mechanism). The {@code /api} base path is appended. + * + *

    Explicit timeouts are set so transient server delays (e.g. SQLite contention + * under load) surface as bounded errors rather than blocking forever. + */ + public static ApiClient client(String serverUrl, String authKey, String authSecret) { + String basePath = normalizeUrl(serverUrl) + "/api"; + ApiClient.ApiClientBuilder builder = ApiClient.builder() + .basePath(basePath) + .connectTimeout(10_000) // ms — fail fast if server unreachable + .readTimeout(30_000) // ms — bound slow server responses + .writeTimeout(30_000); + if (authKey != null && !authKey.isEmpty()) { + builder.credentials(authKey, authSecret); + } + return builder.build(); + } + + private static String envVar(String key, String defaultValue) { + String val = System.getenv(key); + return val != null ? val : defaultValue; + } + + /** Strip a trailing {@code /} and any {@code /api} suffix; defaults if null. */ + private static String normalizeUrl(String url) { + String s = (url != null ? url : "http://localhost:6767").stripTrailing(); + while (s.endsWith("/")) s = s.substring(0, s.length() - 1); + if (s.endsWith("/api")) s = s.substring(0, s.length() - 4); + while (s.endsWith("/")) s = s.substring(0, s.length() - 1); + return s; } // ── Synchronous API ────────────────────────────────────────────────── @@ -91,22 +187,9 @@ public AgentRuntime(AgentConfig config) { * @param agent the agent to compile * @return plan result with workflowDef and requiredWorkers */ - public Map plan(Agent agent) { - Map agentConfig = serializer.serialize(agent); + public CompileResponse plan(Agent agent) { logger.debug("Compiling agent '{}'", agent.getName()); - // Same framework-dispatch as startAsync / deploy: framework-backed - // agents (openai / google_adk / langgraph) need to round-trip through - // the server normalizer or compile fails on a missing top-level model. - String framework = agent.getFramework(); - boolean isFramework = framework != null && !framework.isEmpty(); - Map payload = new java.util.HashMap<>(); - if (isFramework) { - payload.put("framework", framework); - payload.put("rawConfig", agentConfig); - } else { - payload.put("agentConfig", agentConfig); - } - Map result = httpApi.compileAgent(payload); + CompileResponse result = agentClient.compileAgent(agentRequest(agent).build()); logger.info("Agent '{}' compiled successfully", agent.getName()); return result; } @@ -124,7 +207,7 @@ public AgentResult run(Agent agent, String prompt) { /** * Execute a {@code Strategy.PLAN_EXECUTE} harness with a deterministic - * {@link ai.agentspan.plans.Plan} — skips the planner LLM entirely. + * {@link Plan} — skips the planner LLM entirely. * *

    The SDK forwards the plan as {@code static_plan} on the start * payload; the server's PAC extract_json picks it up as Case-0 @@ -137,7 +220,7 @@ public AgentResult run(Agent agent, String prompt) { * @param plan the deterministic plan to execute * @return the agent result */ - public AgentResult run(Agent agent, String prompt, ai.agentspan.plans.Plan plan) { + public AgentResult run(Agent agent, String prompt, Plan plan) { return runAsync(agent, prompt, plan).join(); } @@ -173,20 +256,20 @@ public AgentStream stream(Agent agent, String prompt) { * @return a CompletableFuture that resolves to the agent result */ public CompletableFuture runAsync(Agent agent, String prompt) { - return runAsync(agent, prompt, null); + return runAsync(agent, prompt, (Plan) null); } /** - * Async variant of {@link #run(Agent, String, ai.agentspan.plans.Plan)}. + * Async variant of {@link #run(Agent, String, Plan)}. */ - public CompletableFuture runAsync( - Agent agent, String prompt, ai.agentspan.plans.Plan plan) { - prepareWorkers(agent); - workerManager.startAll(); - - return startAsync(agent, prompt, plan).thenCompose(handle -> - CompletableFuture.supplyAsync(() -> handle.waitForResult()) - ); + public CompletableFuture runAsync(Agent agent, String prompt, Plan plan) { + // Worker registration + runner start happen inside startAsync, under the + // per-execution domain (runId) for stateful agents. Do NOT pre-register + // here without that domain: it would build the runner polling the default + // queue, and the later domain-aware registration could be skipped — leaving + // the worker polling the wrong queue while the server enqueues under runId. + return startAsync(agent, prompt, plan) + .thenCompose(handle -> CompletableFuture.supplyAsync(() -> handle.waitForResult())); } /** @@ -197,56 +280,44 @@ public CompletableFuture runAsync( * @return a CompletableFuture that resolves to an AgentHandle */ public CompletableFuture startAsync(Agent agent, String prompt) { - return startAsync(agent, prompt, null); + return startAsync(agent, prompt, (Plan) null); } /** - * Async variant that forwards a deterministic {@link ai.agentspan.plans.Plan} + * Async variant that forwards a deterministic {@link Plan} * to the server as {@code static_plan}. Only meaningful for * {@code Strategy.PLAN_EXECUTE} harnesses; ignored otherwise. */ - public CompletableFuture startAsync( - Agent agent, String prompt, ai.agentspan.plans.Plan plan) { + public CompletableFuture startAsync(Agent agent, String prompt, Plan plan) { // Stateful agents get a per-execution domain UUID. The server uses it // as taskToDomain for every worker task in this run; local workers are // registered under the same domain so they poll the per-execution // queue. Without this, concurrent stateful runs share a single domain // queue and can dequeue each other's tasks. // Mirrors Python runtime._has_stateful_tools + run_id = uuid.uuid4(). - final String runId = hasStatefulTools(agent) - ? java.util.UUID.randomUUID().toString().replace("-", "") - : null; - final Map staticPlan = plan == null ? null : plan.toJson(); + final String runId = + hasStatefulTools(agent) ? UUID.randomUUID().toString().replace("-", "") : null; prepareWorkers(agent, runId); workerManager.startAll(); return CompletableFuture.supplyAsync(() -> { - Map agentConfig = serializer.serialize(agent); String sessionId = agent.getSessionId(); logger.debug("Starting agent '{}' with prompt: {}", agent.getName(), prompt); - // Framework-backed agents (openai, google_adk, langgraph, vercel_ai, skill) - // must be sent via the server's framework+rawConfig fields so the - // matching normalizer runs server-side. - String framework = agent.getFramework(); - boolean isFramework = framework != null && !framework.isEmpty(); - Map payload = new java.util.HashMap<>(); - if (isFramework) { - payload.put("framework", framework); - payload.put("rawConfig", agentConfig); - } else { - payload.put("agentConfig", agentConfig); + StartResponse response = agentClient.startAgent(agentRequest(agent) + .prompt(prompt) + .sessionId(sessionId != null && !sessionId.isEmpty() ? sessionId : null) + .runId(runId != null && !runId.isEmpty() ? runId : null) + .staticPlan(plan) + .build()); + String executionId = response.getExecutionId(); + if (executionId == null) { + throw new RuntimeException("Server returned no executionId for agent '" + agent.getName() + "'"); } - payload.put("prompt", prompt); - if (sessionId != null && !sessionId.isEmpty()) payload.put("sessionId", sessionId); - if (runId != null && !runId.isEmpty()) payload.put("runId", runId); - if (staticPlan != null) payload.put("static_plan", staticPlan); - Map response = httpApi.startAgent(payload); - String executionId = extractExecutionId(response); logger.info("Agent '{}' started with execution ID: {}", agent.getName(), executionId); - return new AgentHandle(executionId, httpApi); + return new AgentHandle(executionId, agentClient, workflowClient); }); } @@ -266,6 +337,18 @@ private static boolean hasStatefulTools(Agent agent) { return false; } + /** + * Build an {@link AgentRequest} pre-populated with the agent definition. + * The agent's framework string is resolved to a {@link Framework} enum value; + * if it matches a known framework the request uses {@code framework + rawConfig}, + * otherwise it uses {@code agentConfig} (native path). + */ + private static AgentRequest.Builder agentRequest(Agent agent) { + return Framework.of(agent.getFramework()) + .map(fw -> AgentRequest.frameworkAgent(fw, agent)) + .orElseGet(() -> AgentRequest.nativeAgent(agent)); + } + /** * Execute an agent and stream events asynchronously. * @@ -274,17 +357,14 @@ private static boolean hasStatefulTools(Agent agent) { * @return a CompletableFuture that resolves to an AgentStream */ public CompletableFuture streamAsync(Agent agent, String prompt) { - prepareWorkers(agent); - workerManager.startAll(); - + // Worker registration + runner start happen inside startAsync, under the + // per-execution domain (runId) for stateful agents — see runAsync. return startAsync(agent, prompt).thenApply(handle -> { String executionId = handle.getExecutionId(); - String sseUrl = config.getServerUrl() + "/api/agent/stream/" + executionId; - - SseClient sseClient = new SseClient(sseUrl, config, httpApi.getHttpClient()); + SseClient sseClient = new SseClient(conductorClient, executionId); sseClient.connect(); - return new AgentStream(executionId, sseClient, httpApi); + return new AgentStream(executionId, sseClient, agentClient); }); } @@ -300,29 +380,15 @@ public CompletableFuture streamAsync(Agent agent, String prompt) { * @param agents one or more agents to deploy * @return list of DeploymentInfo, one per deployed agent */ - public List deploy(Agent... agents) { + public List deploy(Agent... agents) { if (agents == null || agents.length == 0) { throw new IllegalArgumentException("deploy() requires at least one agent"); } - List results = new ArrayList<>(); + List results = new ArrayList<>(); for (Agent agent : agents) { - Map agentConfig = serializer.serialize(agent); - Map payload = new java.util.LinkedHashMap<>(); - // Framework-backed agents (openai, google_adk, langgraph, skill) ship via - // {framework, rawConfig} so the matching server-side normalizer - // runs — same dispatch as startAsync. Without this, the server - // tries to compile the agent as a native Agentspan agent and - // fails on a missing model / null taskDef name. - String framework = agent.getFramework(); - if (framework != null && !framework.isEmpty()) { - payload.put("framework", framework); - payload.put("rawConfig", agentConfig); - } else { - payload.put("agentConfig", agentConfig); - } - Map resp = httpApi.deployAgent(payload); - String registeredName = resp.getOrDefault("agentName", agent.getName()).toString(); - results.add(new ai.agentspan.model.DeploymentInfo(registeredName, agent.getName())); + StartResponse resp = agentClient.deployAgent(agentRequest(agent).build()); + String registeredName = resp.getAgentName() != null ? resp.getAgentName() : agent.getName(); + results.add(new DeploymentInfo(registeredName, agent.getName())); logger.info("Deployed agent '{}' as '{}'", agent.getName(), registeredName); } return results; @@ -334,7 +400,7 @@ public List deploy(Agent... agents) { * @param agents one or more agents to deploy * @return CompletableFuture resolving to list of DeploymentInfo */ - public CompletableFuture> deployAsync(Agent... agents) { + public CompletableFuture> deployAsync(Agent... agents) { return CompletableFuture.supplyAsync(() -> deploy(agents)); } @@ -350,10 +416,10 @@ public CompletableFuture> deployAsync(Ag * * @param agent the agent to deploy * @param schedules schedules to attach (tri-state semantics above) - * @return the {@link ai.agentspan.model.DeploymentInfo} + * @return the {@link DeploymentInfo} */ - public ai.agentspan.model.DeploymentInfo deploy(Agent agent, List schedules) { - List infos = deploy(new Agent[] {agent}); + public DeploymentInfo deploy(Agent agent, List schedules) { + List infos = deploy(new Agent[] {agent}); if (schedules != null) { schedules().reconcile(agent.getName(), schedules); } @@ -365,7 +431,7 @@ public Schedules schedules() { if (schedules == null) { synchronized (this) { if (schedules == null) { - schedules = new Schedules(config, httpApi.getHttpClient()); + schedules = new Schedules(conductorClient); } } } @@ -382,8 +448,7 @@ public Schedules schedules() { */ public void serve(Agent... agents) { if (agents == null || agents.length == 0) { - throw new IllegalArgumentException( - "serve() requires at least one agent — without one, no workers would " + throw new IllegalArgumentException("serve() requires at least one agent — without one, no workers would " + "register and the call would block forever."); } for (Agent agent : agents) { @@ -403,15 +468,15 @@ public void serve(Agent... agents) { * Re-attach to an existing agent execution and re-register workers. * *

    Fetches the workflow from the server and re-registers tool workers. - * Returns an {@link ai.agentspan.model.AgentHandle} for continued interaction. + * Returns an {@link AgentHandle} for continued interaction. * * @param executionId the execution ID from a previous {@link #start} call * @param agent the same Agent definition that was originally executed * @return an AgentHandle bound to this runtime */ - public ai.agentspan.model.AgentHandle resume(String executionId, Agent agent) { + public AgentHandle resume(String executionId, Agent agent) { prepareWorkers(agent); - return new ai.agentspan.model.AgentHandle(executionId, httpApi); + return new AgentHandle(executionId, agentClient, workflowClient); } /** @@ -421,18 +486,31 @@ public ai.agentspan.model.AgentHandle resume(String executionId, Agent agent) { * @param agent the agent definition originally executed * @return CompletableFuture resolving to an AgentHandle */ - public CompletableFuture resumeAsync(String executionId, Agent agent) { + public CompletableFuture resumeAsync(String executionId, Agent agent) { return CompletableFuture.supplyAsync(() -> resume(executionId, agent)); } // ── Lifecycle ──────────────────────────────────────────────────────── /** - * Shutdown the runtime, stopping all worker threads. + * Shutdown the runtime, stopping all worker threads and releasing HTTP connections. + * + *

    Without explicit cleanup, the OkHttpClient's dispatcher thread pool and connection + * pool remain alive after the workers stop. Over many sequential test suites this + * accumulates idle threads and held connections, which increases server-side load and + * can delay status-poll responses on a shared SQLite-backed server. */ public void shutdown() { logger.info("Shutting down AgentRuntime"); workerManager.stop(); + try { + // Release the HTTP connection pool and dispatcher thread pool. Without this, + // each closed AgentRuntime leaves idle OkHttp threads alive (60s keepalive) + // and held connections, which accumulate over many sequential test suites. + conductorClient.shutdown(); + } catch (Exception e) { + logger.debug("Error releasing HTTP client resources: {}", e.getMessage()); + } } @Override @@ -440,6 +518,235 @@ public void close() { shutdown(); } + // ── Drop-in support for native framework agents (run / start / stream / + // deploy / serve / plan / resume all accept the raw native object) ── + + /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ + public AgentResult run(Object agent, String prompt) { + return run(coerceAgent(agent), prompt); + } + + /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ + public CompletableFuture runAsync(Object agent, String prompt) { + return runAsync(coerceAgent(agent), prompt); + } + + /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ + public AgentHandle start(Object agent, String prompt) { + return start(coerceAgent(agent), prompt); + } + + /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ + public CompletableFuture startAsync(Object agent, String prompt) { + return startAsync(coerceAgent(agent), prompt); + } + + /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ + public AgentStream stream(Object agent, String prompt) { + return stream(coerceAgent(agent), prompt); + } + + /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ + public CompletableFuture streamAsync(Object agent, String prompt) { + return streamAsync(coerceAgent(agent), prompt); + } + + /** Drop-in: accepts native ADK {@code BaseAgent} instances (or Agentspan {@link Agent}s). */ + public List deploy(Object... agents) { + return deploy(coerceAgents(agents)); + } + + /** Drop-in: accepts native ADK {@code BaseAgent} instances (or Agentspan {@link Agent}s). */ + public CompletableFuture> deployAsync(Object... agents) { + return deployAsync(coerceAgents(agents)); + } + + /** Drop-in: accepts native ADK {@code BaseAgent} instances (or Agentspan {@link Agent}s). */ + public void serve(Object... agents) { + serve(coerceAgents(agents)); + } + + /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ + public CompileResponse plan(Object agent) { + return plan(coerceAgent(agent)); + } + + /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ + public AgentHandle resume(String executionId, Object agent) { + return resume(executionId, coerceAgent(agent)); + } + + /** Drop-in: accepts a native ADK {@code BaseAgent} or any Agentspan {@link Agent}. */ + public CompletableFuture resumeAsync(String executionId, Object agent) { + return resumeAsync(executionId, coerceAgent(agent)); + } + + // Tool-bearing drop-ins for native LangChain4j {@code ChatModel} / LangGraph4j + // {@code AgentExecutor.Builder} + {@code @Tool} POJOs. These take {@code Object} + // (not the framework types) so the core class never references compileOnly + // LangChain4j/LangGraph4j classes in a signature — important because Spring + // introspects this bean's methods, which would otherwise force-resolve those + // optional types. The native object is dispatched reflectively in coerceAgent. + // Fixed-arity overloads (run(Agent,String), run(Object,String)) still win + // resolution, so these only apply to 3+ arg framework calls. + + /** Drop-in: native LangChain4j {@code ChatModel} / LangGraph4j {@code Builder} + tool POJOs. */ + public AgentResult run(Object agent, String prompt, Object... tools) { + return run(coerceAgent(agent, tools), prompt); + } + + /** Drop-in (async): native framework object + tool POJOs. */ + public CompletableFuture runAsync(Object agent, String prompt, Object... tools) { + return runAsync(coerceAgent(agent, tools), prompt); + } + + /** Drop-in (start): native framework object + tool POJOs. */ + public AgentHandle start(Object agent, String prompt, Object... tools) { + return start(coerceAgent(agent, tools), prompt); + } + + /** Drop-in (stream): native framework object + tool POJOs. */ + public AgentStream stream(Object agent, String prompt, Object... tools) { + return stream(coerceAgent(agent, tools), prompt); + } + + /** + * Coerce a user-provided agent object to an Agentspan {@link Agent}. + * + *

    Supports {@link Agent} (returned as-is) and these native framework objects, each + * detected by fully-qualified-name so the core class never hard-references a + * compileOnly framework type in a signature (resolution happens only in the method + * body, when such an object is actually passed and the framework is on the classpath): + *

      + *
    • {@code com.google.adk.agents.BaseAgent} → {@code AdkBridge}
    • + *
    • {@code dev.langchain4j.model.chat.ChatModel} → {@code LangChainBridge}
    • + *
    • {@code org.bsc.langgraph4j.agentexecutor.AgentExecutor$Builder} → {@code LangChainBridge}
    • + *
    + */ + private static Agent coerceAgent(Object agent) { + return coerceAgent(agent, new Object[0]); + } + + private static Agent coerceAgent(Object agent, Object[] tools) { + if (agent == null) { + throw new IllegalArgumentException("agent is null"); + } + if (agent instanceof Agent a) { + return a; + } + if (isInstanceOf(agent, "com.google.adk.agents.BaseAgent")) { + return org.conductoross.conductor.ai.frameworks.AdkBridge.toAgentspan( + (com.google.adk.agents.BaseAgent) agent); + } + if (isInstanceOf(agent, "dev.langchain4j.model.chat.ChatModel")) { + return buildLangchainAgent(agent, tools); + } + if (isInstanceOf(agent, "org.bsc.langgraph4j.agentexecutor.AgentExecutor$Builder")) { + return buildLanggraphAgent(agent, tools); + } + throw new IllegalArgumentException( + "Unsupported agent type: " + agent.getClass().getName() + + ". Expected org.conductoross.conductor.ai.Agent, a native ADK BaseAgent, " + + "a LangChain4j ChatModel, or a LangGraph4j AgentExecutor.Builder."); + } + + private static Agent buildLangchainAgent(Object model, Object[] tools) { + return org.conductoross.conductor.ai.frameworks.LangChainBridge.agentBuilder( + "langchain_agent", + (dev.langchain4j.model.chat.ChatModel) model, + null, + tools == null ? new Object[0] : tools) + .build(); + } + + private static Agent buildLanggraphAgent(Object builderObj, Object[] tools) { + // The LangGraph4j AgentExecutor.Builder carries the ChatModel and the (optional) + // SystemMessage in package-private fields; recover them reflectively. A future + // LangGraph4j build that changes the shape fails loudly rather than degrading. + org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder builder = + (org.bsc.langgraph4j.agentexecutor.AgentExecutor.Builder) builderObj; + dev.langchain4j.model.chat.ChatModel model = + readBuilderField(builder, "chatModel", dev.langchain4j.model.chat.ChatModel.class); + if (model == null) { + throw new IllegalArgumentException("run(AgentExecutor.Builder, ...): the Builder has no chatModel set. " + + "Call .chatModel(...) before handing the Builder to the runtime."); + } + String systemText = readSystemMessageText(builder); + // Validate the Builder produces a compilable LangGraph4j StateGraph before shipping the config. + try { + builder.build(); + } catch (Exception e) { + throw new RuntimeException( + "AgentExecutor.Builder is not a valid LangGraph4j configuration: " + e.getMessage(), e); + } + return org.conductoross.conductor.ai.frameworks.LangChainBridge.agentBuilder( + "langgraph_agent", model, systemText, tools == null ? new Object[0] : tools) + .build(); + } + + @SuppressWarnings("unchecked") + private static T readBuilderField(Object o, String fieldName, Class expected) { + try { + java.lang.reflect.Field f = o.getClass().getDeclaredField(fieldName); + f.setAccessible(true); + Object v = f.get(o); + return expected.isInstance(v) ? (T) v : null; + } catch (NoSuchFieldException nsf) { + return null; + } catch (Throwable t) { + throw new RuntimeException( + "AgentExecutor.Builder field '" + fieldName + + "' is no longer accessible — likely a LangGraph4j upgrade. Open an issue.", + t); + } + } + + private static String readSystemMessageText(Object builder) { + try { + java.lang.reflect.Field f = builder.getClass().getDeclaredField("systemMessage"); + f.setAccessible(true); + Object sys = f.get(builder); + if (sys == null) return null; + java.lang.reflect.Method m = sys.getClass().getMethod("text"); + Object t = m.invoke(sys); + return t instanceof String s && !s.isEmpty() ? s : null; + } catch (Throwable t) { + return null; + } + } + + private static Agent[] coerceAgents(Object[] agents) { + if (agents == null) return new Agent[0]; + Agent[] out = new Agent[agents.length]; + for (int i = 0; i < agents.length; i++) { + try { + out[i] = coerceAgent(agents[i]); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("agents[" + i + "]: " + e.getMessage(), e); + } + } + return out; + } + + /** + * Walk the entire type hierarchy looking for a type whose FQN matches, so the + * dispatcher compiles and runs without ADK on the classpath — only callers + * actually passing native ADK objects trigger the JVM to load ADK classes. + */ + private static boolean isInstanceOf(Object o, String fqn) { + return matchesType(o.getClass(), fqn); + } + + private static boolean matchesType(Class c, String fqn) { + if (c == null) return false; + if (fqn.equals(c.getName())) return true; + if (matchesType(c.getSuperclass(), fqn)) return true; + for (Class i : c.getInterfaces()) { + if (matchesType(i, fqn)) return true; + } + return false; + } + // ── Internal ───────────────────────────────────────────────────────── /** @@ -481,7 +788,8 @@ public void prepareWorkers(Agent agent) { tool.getName(), tool.getFunc(), workerManager.getCurrentDomain(), - tool.getCredentials()); + tool.getCredentials(), + tool.getTimeoutSeconds()); } // Recursively prepare workers for agent_tool child agents if ("agent_tool".equals(tool.getToolType()) && tool.getAgentRef() != null) { @@ -491,17 +799,17 @@ public void prepareWorkers(Agent agent) { // Register callback workers (legacy single-function style) if (agent.getBeforeModelCallback() != null) { - final java.util.function.Function, Map> beforeCb = agent.getBeforeModelCallback(); + final Function, Map> beforeCb = agent.getBeforeModelCallback(); workerManager.register(agent.getName() + "_before_model", inputData -> { Map result = beforeCb.apply(inputData); - return result != null ? result : java.util.Collections.emptyMap(); + return result != null ? result : Collections.emptyMap(); }); } if (agent.getAfterModelCallback() != null) { - final java.util.function.Function, Map> afterCb = agent.getAfterModelCallback(); + final Function, Map> afterCb = agent.getAfterModelCallback(); workerManager.register(agent.getName() + "_after_model", inputData -> { Map result = afterCb.apply(inputData); - return result != null ? result : java.util.Collections.emptyMap(); + return result != null ? result : Collections.emptyMap(); }); } @@ -511,11 +819,11 @@ public void prepareWorkers(Agent agent) { final String agentName = agent.getName(); String[][] positionMethods = { {"before_agent", "onAgentStart"}, - {"after_agent", "onAgentEnd"}, + {"after_agent", "onAgentEnd"}, {"before_model", "onModelStart"}, - {"after_model", "onModelEnd"}, - {"before_tool", "onToolStart"}, - {"after_tool", "onToolEnd"}, + {"after_model", "onModelEnd"}, + {"before_tool", "onToolStart"}, + {"after_tool", "onToolEnd"}, }; for (String[] pm : positionMethods) { String position = pm[0]; @@ -524,11 +832,12 @@ public void prepareWorkers(Agent agent) { List active = new ArrayList<>(); for (CallbackHandler h : handlers) { try { - java.lang.reflect.Method m = h.getClass().getMethod(methodName, Map.class); + Method m = h.getClass().getMethod(methodName, Map.class); if (!m.getDeclaringClass().equals(CallbackHandler.class)) { active.add(h); } - } catch (NoSuchMethodException ignored) {} + } catch (NoSuchMethodException ignored) { + } } if (active.isEmpty()) continue; @@ -539,7 +848,7 @@ public void prepareWorkers(Agent agent) { workerManager.register(taskName, inputData -> { for (CallbackHandler handler : activeHandlers) { try { - java.lang.reflect.Method m = handler.getClass().getMethod(mName, Map.class); + Method m = handler.getClass().getMethod(mName, Map.class); m.setAccessible(true); // allow invocation on package-private inner classes Map result = (Map) m.invoke(handler, inputData); if (result != null && !result.isEmpty()) return result; @@ -547,30 +856,30 @@ public void prepareWorkers(Agent agent) { logger.warn("CallbackHandler {} failed for {}: {}", mName, taskName, e.getMessage()); } } - return java.util.Collections.emptyMap(); + return Collections.emptyMap(); }); } } // Register combined guardrail worker per agent (matches Python: {agent_name}_output_guardrail) - List customGuardrails = agent.getGuardrails().stream() - .filter(g -> g.getFunc() != null) - .collect(java.util.stream.Collectors.toList()); + List customGuardrails = + agent.getGuardrails().stream().filter(g -> g.getFunc() != null).collect(Collectors.toList()); if (!customGuardrails.isEmpty()) { String taskName = agent.getName() + "_output_guardrail"; workerManager.register(taskName, inputData -> { Object rawContent = inputData.get("content"); String content = rawContent != null ? rawContent.toString() : ""; int iteration = inputData.get("iteration") instanceof Number - ? ((Number) inputData.get("iteration")).intValue() : 0; - for (ai.agentspan.model.GuardrailDef g : customGuardrails) { - ai.agentspan.model.GuardrailResult result = g.getFunc().apply(content); + ? ((Number) inputData.get("iteration")).intValue() + : 0; + for (GuardrailDef g : customGuardrails) { + GuardrailResult result = g.getFunc().apply(content); if (!result.isPassed()) { String onFail = g.getOnFail().toJsonValue(); String fixedOutput = result.getFixedOutput(); if ("retry".equals(onFail) && iteration >= g.getMaxRetries()) onFail = "raise"; if ("fix".equals(onFail) && fixedOutput == null) onFail = "raise"; - Map out = new java.util.LinkedHashMap<>(); + Map out = new LinkedHashMap<>(); out.put("passed", false); out.put("message", result.getMessage() != null ? result.getMessage() : ""); out.put("on_fail", onFail); @@ -580,7 +889,7 @@ public void prepareWorkers(Agent agent) { return out; } } - Map out = new java.util.LinkedHashMap<>(); + Map out = new LinkedHashMap<>(); out.put("passed", true); out.put("message", ""); out.put("on_fail", "pass"); @@ -597,11 +906,12 @@ public void prepareWorkers(Agent agent) { final String taskName = agent.getName() + "_termination"; workerManager.register(taskName, input -> { String result = input.get("result") instanceof String - ? (String) input.get("result") : (input.get("result") != null ? input.get("result").toString() : ""); - int iteration = input.get("iteration") instanceof Number - ? ((Number) input.get("iteration")).intValue() : 0; + ? (String) input.get("result") + : (input.get("result") != null ? input.get("result").toString() : ""); + int iteration = + input.get("iteration") instanceof Number ? ((Number) input.get("iteration")).intValue() : 0; boolean shouldContinue = evaluateTermination(termination, result, iteration); - Map out = new java.util.LinkedHashMap<>(); + Map out = new LinkedHashMap<>(); out.put("should_continue", shouldContinue); out.put("reason", shouldContinue ? "" : "Termination condition met"); return out; @@ -611,19 +921,37 @@ public void prepareWorkers(Agent agent) { // Register local code execution worker if (agent.isLocalCodeExecution()) { String taskName = agent.getName() + "_execute_code"; - workerManager.register(taskName, inputData -> { - String language = inputData.get("language") instanceof String - ? (String) inputData.get("language") : "python"; - String code = inputData.get("code") instanceof String - ? (String) inputData.get("code") : ""; - int timeout = agent.getCodeExecutionTimeout() > 0 ? agent.getCodeExecutionTimeout() : 30; - return executeCode(language, code, timeout); - }); + int codeTimeout = agent.getCodeExecutionTimeout() > 0 ? agent.getCodeExecutionTimeout() : 30; + workerManager.register( + taskName, + inputData -> { + String language = inputData.get("language") instanceof String + ? (String) inputData.get("language") + : "python"; + String code = inputData.get("code") instanceof String ? (String) inputData.get("code") : ""; + return executeCode(language, code, codeTimeout); + }, + workerManager.getCurrentDomain(), + Collections.emptyList(), + codeTimeout); + } + + // Register local CLI command execution worker. Mirrors Python's + // Agent._attach_cli_tool(): the {name}_run_command tool runs whitelisted + // commands locally via CliCommandExecutor. + if (agent.getCliConfig() != null && agent.getCliConfig().isEnabled()) { + final CliConfig cliCfg = agent.getCliConfig(); + String taskName = agent.getName() + "_run_command"; + workerManager.register( + taskName, + inputData -> CliCommandExecutor.run(inputData, cliCfg), + workerManager.getCurrentDomain(), + Collections.emptyList(), + cliCfg.getTimeout()); } // Register SWARM transfer workers - if (ai.agentspan.enums.Strategy.SWARM.equals(agent.getStrategy()) - && !agent.getAgents().isEmpty()) { + if (Strategy.SWARM.equals(agent.getStrategy()) && !agent.getAgents().isEmpty()) { registerSwarmWorkers(agent); } @@ -631,8 +959,7 @@ public void prepareWorkers(Agent agent) { // The server creates a {name}_process_selection SIMPLE task after each // HUMAN pick-agent task. This worker maps the selected agent name to its // positional index (used by the SWITCH task to route to the right sub-agent). - if (ai.agentspan.enums.Strategy.MANUAL.equals(agent.getStrategy()) - && !agent.getAgents().isEmpty()) { + if (Strategy.MANUAL.equals(agent.getStrategy()) && !agent.getAgents().isEmpty()) { registerManualWorkers(agent); } @@ -703,7 +1030,7 @@ private void registerSwarmWorkers(Agent swarmAgent) { for (String peer : allNames) { if (!source.equals(peer)) { final String taskName = source + "_transfer_to_" + peer; - workerManager.register(taskName, input -> java.util.Collections.emptyMap()); + workerManager.register(taskName, input -> Collections.emptyMap()); } } } @@ -716,7 +1043,7 @@ private void registerSwarmWorkers(Agent swarmAgent) { final String taskName = agentName + "_check_transfer"; workerManager.register(taskName, input -> { Object toolCallsRaw = input.get("tool_calls"); - Map out = new java.util.LinkedHashMap<>(); + Map out = new LinkedHashMap<>(); out.put("is_transfer", false); out.put("transfer_to", ""); if (toolCallsRaw instanceof List) { @@ -758,13 +1085,12 @@ private void registerSwarmWorkers(Agent swarmAgent) { workerManager.register(handoffTaskName, input -> { Object isTransferRaw = input.get("is_transfer"); boolean isTransfer = Boolean.TRUE.equals(isTransferRaw) - || "true".equalsIgnoreCase(isTransferRaw != null ? isTransferRaw.toString() : ""); - String transferTo = input.get("transfer_to") instanceof String - ? (String) input.get("transfer_to") : ""; - String currentAgent = input.get("active_agent") instanceof String - ? (String) input.get("active_agent") : "0"; + || "true".equalsIgnoreCase(isTransferRaw != null ? isTransferRaw.toString() : ""); + String transferTo = input.get("transfer_to") instanceof String ? (String) input.get("transfer_to") : ""; + String currentAgent = + input.get("active_agent") instanceof String ? (String) input.get("active_agent") : "0"; - Map out = new java.util.LinkedHashMap<>(); + Map out = new LinkedHashMap<>(); if (isTransfer && !transferTo.isEmpty()) { // Find the case index for the target agent int targetIdx = subNames.indexOf(transferTo); @@ -804,7 +1130,7 @@ private void registerSwarmWorkers(Agent swarmAgent) { @SuppressWarnings("unchecked") private void registerManualWorkers(Agent manualAgent) { List subAgents = manualAgent.getAgents(); - Map nameToIndex = new java.util.HashMap<>(); + Map nameToIndex = new HashMap<>(); for (int i = 0; i < subAgents.size(); i++) { nameToIndex.put(subAgents.get(i).getName(), String.valueOf(i)); } @@ -824,27 +1150,36 @@ private void registerManualWorkers(Agent manualAgent) { logger.warn("MANUAL process_selection: unknown agent '{}'; defaulting to 0", selected); index = "0"; } - Map out = new java.util.HashMap<>(); + Map out = new HashMap<>(); out.put("selected", index); return out; }); } private Map executeCode(String language, String code, int timeoutSeconds) { - Map result = new java.util.LinkedHashMap<>(); + Map result = new LinkedHashMap<>(); try { String interpreter; switch (language.toLowerCase()) { - case "python": case "python3": interpreter = "python3"; break; - case "bash": case "sh": interpreter = "bash"; break; - case "node": case "javascript": interpreter = "node"; break; - default: interpreter = language; + case "python": + case "python3": + interpreter = "python3"; + break; + case "bash": + case "sh": + interpreter = "bash"; + break; + case "node": + case "javascript": + interpreter = "node"; + break; + default: + interpreter = language; } // Write code to temp file - java.io.File tmpFile = java.io.File.createTempFile("agentspan_code_", - language.startsWith("python") ? ".py" : ".sh"); + File tmpFile = File.createTempFile("agentspan_code_", language.startsWith("python") ? ".py" : ".sh"); tmpFile.deleteOnExit(); - try (java.io.FileWriter fw = new java.io.FileWriter(tmpFile)) { + try (FileWriter fw = new FileWriter(tmpFile)) { fw.write(code); } @@ -852,7 +1187,7 @@ private Map executeCode(String language, String code, int timeou pb.redirectErrorStream(true); Process process = pb.start(); - boolean finished = process.waitFor(timeoutSeconds, java.util.concurrent.TimeUnit.SECONDS); + boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS); String output = new String(process.getInputStream().readAllBytes()); if (!finished) { @@ -878,25 +1213,4 @@ private Map executeCode(String language, String code, int timeou } return result; } - - private String extractExecutionId(Map response) { - Object id = response.get("executionId"); - if (id != null) return id.toString(); - - // Legacy fallback — older server versions may still return workflowId - id = response.get("workflowId"); - if (id != null) return id.toString(); - - id = response.get("id"); - if (id != null) return id.toString(); - - id = response.get("correlationId"); - if (id != null) return id.toString(); - - if (response.size() == 1) { - return response.values().iterator().next().toString(); - } - - throw new RuntimeException("Cannot extract execution ID from response: " + response); - } } diff --git a/sdk/java/src/main/java/ai/agentspan/CallbackHandler.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/CallbackHandler.java similarity index 90% rename from sdk/java/src/main/java/ai/agentspan/CallbackHandler.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/CallbackHandler.java index c4838dd9d..be85f675c 100644 --- a/sdk/java/src/main/java/ai/agentspan/CallbackHandler.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/CallbackHandler.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan; +package org.conductoross.conductor.ai; import java.util.Map; @@ -40,20 +40,32 @@ public abstract class CallbackHandler { /** Called before the agent begins processing. Return non-null non-empty map to override. */ - public Map onAgentStart(Map kwargs) { return null; } + public Map onAgentStart(Map kwargs) { + return null; + } /** Called after the agent finishes processing. Return non-null non-empty map to override. */ - public Map onAgentEnd(Map kwargs) { return null; } + public Map onAgentEnd(Map kwargs) { + return null; + } /** Called before each LLM call. Return non-null non-empty map to short-circuit the LLM. */ - public Map onModelStart(Map kwargs) { return null; } + public Map onModelStart(Map kwargs) { + return null; + } /** Called after each LLM call. Return non-null non-empty map to replace the response. */ - public Map onModelEnd(Map kwargs) { return null; } + public Map onModelEnd(Map kwargs) { + return null; + } /** Called before each tool execution. Return non-null non-empty map to override. */ - public Map onToolStart(Map kwargs) { return null; } + public Map onToolStart(Map kwargs) { + return null; + } /** Called after each tool execution. Return non-null non-empty map to override. */ - public Map onToolEnd(Map kwargs) { return null; } + public Map onToolEnd(Map kwargs) { + return null; + } } diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/AgentDef.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/AgentDef.java new file mode 100644 index 000000000..98945c668 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/AgentDef.java @@ -0,0 +1,133 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.conductoross.conductor.ai.enums.Strategy; + +/** + * Marks a method as an agent definition. + * + *

    The Java counterpart of the Python SDK's {@code @agent} decorator. Annotate a + * method to define an agent declaratively; resolve it into an + * {@link org.conductoross.conductor.ai.Agent} with + * {@link org.conductoross.conductor.ai.Agent#fromInstance(Object)} or + * {@link org.conductoross.conductor.ai.Agent#fromInstance(Object, String)}. + * + *

    The method's return type declares what it provides: + *

      + *
    • {@code void} — nothing; the annotation attributes alone define the agent.
    • + *
    • {@code String} — dynamic instructions. A no-arg method is lazy: it is + * re-invoked every time the agent config is serialized (each run submission), + * so the prompt can reflect current state — matching the Python SDK, where + * callable instructions resolve at serialization time. A non-empty result wins + * over the {@link #instructions()} attribute.
    • + *
    • {@code PromptTemplate} — a server-side instructions template + * (sets {@code instructionsTemplate}); invoked once.
    • + *
    • {@code Agent.Builder} — the definition itself; the returned builder is built.
    • + *
    • {@code Agent} — the definition itself, returned as-is (CrewAI-style full + * factory). For no-arg factory forms, annotation attributes other than + * {@link #name()} are rejected — they would be silently ignored.
    • + *
    + * + *

    The method may take no parameters, or a single + * {@link org.conductoross.conductor.ai.Agent.Builder Agent.Builder} parameter as an + * escape hatch: the builder arrives pre-populated from the annotation (and the + * discovered tools/guardrails/sub-agents), and the method body can apply anything + * the builder supports — termination conditions, handoffs, memory, sub-agents from + * other classes, etc. Builder-param methods are invoked exactly once (re-running a + * customizer per serialization would replay its side effects). + * + *

    {@link Tool} and {@link GuardrailDef} methods declared on the same object are + * attached to the agent automatically (all of them by default — see {@link #tools()} + * and {@link #guardrails()}). Sub-agents are referenced by name via {@link #agents()}. + * + *

    Example: + *

    {@code
    + * public class Weather {
    + *     @Tool(description = "Get weather for a city")
    + *     public String getWeather(String city) { return "Sunny, 72F in " + city; }
    + *
    + *     @AgentDef(model = "openai/gpt-4o")
    + *     public String weatherbot() {
    + *         return "You are a weather assistant. Today is " + LocalDate.now() + ".";
    + *     }
    + *
    + *     // builder customizer: full builder API available
    + *     @AgentDef(model = "openai/gpt-4o", instructions = "You are a researcher.")
    + *     public void researcher(Agent.Builder builder) {
    + *         builder.termination(new MaxMessageTermination(10))
    + *                .agents(Agent.fromInstance(new Editing(), "editor"));
    + *     }
    + *
    + *     // full factory: the method builds the whole definition
    + *     @AgentDef
    + *     public Agent reviewer() {
    + *         return Agent.builder().name("reviewer").model("openai/gpt-4o")
    + *                 .instructions("Review the draft.").build();
    + *     }
    + * }
    + *
    + * Agent agent = Agent.fromInstance(new Weather(), "weatherbot");
    + * }
    + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface AgentDef { + /** Agent name. Defaults to the method name if not specified. */ + String name() default ""; + + /** + * LLM model in "provider/model" format (e.g. "openai/gpt-4o"). + * When empty and this agent is referenced as a sub-agent, the parent's + * model is inherited at resolution time. + */ + String model() default ""; + + /** + * Static system prompt. A non-empty {@code String} returned by the annotated + * method takes precedence over this attribute. + */ + String instructions() default ""; + + /** + * Names of {@link Tool}-annotated methods on the same object to attach. + * The default {@code {"*"}} attaches all of them; an empty array attaches none. + */ + String[] tools() default {"*"}; + + /** + * Names of {@link GuardrailDef}-annotated methods on the same object to attach. + * The default {@code {"*"}} attaches all of them; an empty array attaches none. + */ + String[] guardrails() default {"*"}; + + /** + * Names of other {@code @AgentDef}-annotated methods on the same object to use + * as sub-agents for multi-agent orchestration. + */ + String[] agents() default {}; + + /** Multi-agent orchestration strategy. Only meaningful when {@link #agents()} is set. */ + Strategy strategy() default Strategy.HANDOFF; + + /** Maximum number of agent loop iterations. */ + int maxTurns() default 25; + + /** Maximum tokens for LLM generation. 0 means unset (server default applies). */ + int maxTokens() default 0; + + /** Sampling temperature. NaN means unset (server default applies). */ + double temperature() default Double.NaN; + + /** Agent-level credential names to inject into the execution context. */ + String[] credentials() default {}; + + /** Token budget for proactive context condensation. 0 means unset. */ + int contextWindowBudget() default 0; +} diff --git a/sdk/java/src/main/java/ai/agentspan/annotations/GuardrailDef.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/GuardrailDef.java similarity index 84% rename from sdk/java/src/main/java/ai/agentspan/annotations/GuardrailDef.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/GuardrailDef.java index abfc8e8aa..d168b8d52 100644 --- a/sdk/java/src/main/java/ai/agentspan/annotations/GuardrailDef.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/GuardrailDef.java @@ -1,21 +1,21 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.annotations; - -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; +package org.conductoross.conductor.ai.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; + /** * Marks a method as a guardrail function. * *

    Guardrail methods must accept a {@code String} argument (the content to check) - * and return a {@link ai.agentspan.model.GuardrailResult}. + * and return a {@link org.conductoross.conductor.ai.model.GuardrailResult}. * *

    Example: *

    {@code
    diff --git a/sdk/java/src/main/java/ai/agentspan/annotations/Tool.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/Tool.java
    similarity index 92%
    rename from sdk/java/src/main/java/ai/agentspan/annotations/Tool.java
    rename to sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/Tool.java
    index 4189762c8..e33db248d 100644
    --- a/sdk/java/src/main/java/ai/agentspan/annotations/Tool.java
    +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/annotations/Tool.java
    @@ -1,7 +1,7 @@
     // Copyright (c) 2025 Agentspan
     // Licensed under the MIT License. See LICENSE file in the project root for details.
     
    -package ai.agentspan.annotations;
    +package org.conductoross.conductor.ai.annotations;
     
     import java.lang.annotation.ElementType;
     import java.lang.annotation.Retention;
    @@ -12,7 +12,7 @@
      * Marks a method as an agent tool.
      *
      * 

    Use this annotation on methods in a class to register them as callable tools - * for an agent. Use {@link ai.agentspan.internal.ToolRegistry#fromInstance(Object)} + * for an agent. Use {@link org.conductoross.conductor.ai.internal.ToolRegistry#fromInstance(Object)} * to discover all annotated methods. * *

    Example: diff --git a/sdk/java/src/main/java/ai/agentspan/enums/AgentStatus.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/AgentStatus.java similarity index 85% rename from sdk/java/src/main/java/ai/agentspan/enums/AgentStatus.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/enums/AgentStatus.java index 8f8bee106..600e73eec 100644 --- a/sdk/java/src/main/java/ai/agentspan/enums/AgentStatus.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/AgentStatus.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.enums; +package org.conductoross.conductor.ai.enums; /** * Terminal status of an agent workflow execution. diff --git a/sdk/java/src/main/java/ai/agentspan/enums/EventType.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/EventType.java similarity index 81% rename from sdk/java/src/main/java/ai/agentspan/enums/EventType.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/enums/EventType.java index c43a4ef21..c5e427a90 100644 --- a/sdk/java/src/main/java/ai/agentspan/enums/EventType.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/EventType.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.enums; +package org.conductoross.conductor.ai.enums; import com.fasterxml.jackson.annotation.JsonProperty; @@ -41,8 +41,10 @@ public enum EventType { public String toJsonValue() { try { - return EventType.class.getField(name()) - .getAnnotation(JsonProperty.class).value(); + return EventType.class + .getField(name()) + .getAnnotation(JsonProperty.class) + .value(); } catch (NoSuchFieldException e) { return name().toLowerCase(); } diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/Framework.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/Framework.java new file mode 100644 index 000000000..32fd34674 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/Framework.java @@ -0,0 +1,68 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.enums; + +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Framework identifiers for agents backed by a third-party agent SDK. + * + *

    The server routes each framework through a matching {@code AgentConfigNormalizer} + * before compilation. Every value here corresponds to a {@code frameworkId()} in the + * server's normalizer registry. Native Agentspan agents have no framework — their + * config is sent as {@code agentConfig}. + * + *

    Known server normalizers and their IDs: + *

      + *
    • {@code OpenAINormalizer} → {@link #OPENAI}
    • + *
    • {@code GoogleADKNormalizer} → {@link #GOOGLE_ADK}
    • + *
    • {@code LangChainNormalizer} → {@link #LANGCHAIN}
    • + *
    • {@code LangGraphNormalizer} → {@link #LANGGRAPH}
    • + *
    • {@code SkillNormalizer} → {@link #SKILL}
    • + *
    • {@code VercelAINormalizer} → {@link #VERCEL_AI}
    • + *
    • {@code ClaudeAgentSdkNormalizer} → {@link #CLAUDE_AGENT_SDK}
    • + *
    + * + * @see org.conductoross.conductor.ai.frameworks.OpenAIAgent + * @see org.conductoross.conductor.ai.frameworks.AdkBridge + * @see org.conductoross.conductor.ai.frameworks.LangChainBridge + * @see org.conductoross.conductor.ai.skill.Skill + */ +public enum Framework { + OPENAI("openai"), + GOOGLE_ADK("google_adk"), + LANGCHAIN("langchain"), + LANGGRAPH("langgraph"), + SKILL("skill"), + VERCEL_AI("vercel_ai"), + CLAUDE_AGENT_SDK("claude_agent_sdk"); + + private final String wireValue; + + Framework(String wireValue) { + this.wireValue = wireValue; + } + + /** The JSON/wire string value sent to and expected by the server. */ + @JsonValue + public String wireValue() { + return wireValue; + } + + /** + * Return the {@code Framework} matching {@code value}, or empty if + * {@code value} is null, blank, or not a recognised framework identifier. + */ + @JsonCreator + public static Optional of(String value) { + if (value == null || value.isBlank()) return Optional.empty(); + for (Framework f : values()) { + if (f.wireValue.equals(value)) return Optional.of(f); + } + return Optional.empty(); + } +} diff --git a/sdk/java/src/main/java/ai/agentspan/enums/OnFail.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/OnFail.java similarity index 73% rename from sdk/java/src/main/java/ai/agentspan/enums/OnFail.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/enums/OnFail.java index b645f112a..2336db80c 100644 --- a/sdk/java/src/main/java/ai/agentspan/enums/OnFail.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/OnFail.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.enums; +package org.conductoross.conductor.ai.enums; import com.fasterxml.jackson.annotation.JsonProperty; @@ -23,8 +23,10 @@ public enum OnFail { public String toJsonValue() { try { - return OnFail.class.getField(name()) - .getAnnotation(JsonProperty.class).value(); + return OnFail.class + .getField(name()) + .getAnnotation(JsonProperty.class) + .value(); } catch (NoSuchFieldException e) { return name().toLowerCase(); } diff --git a/sdk/java/src/main/java/ai/agentspan/enums/Position.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/Position.java similarity index 70% rename from sdk/java/src/main/java/ai/agentspan/enums/Position.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/enums/Position.java index 5767576a5..3f9d00efb 100644 --- a/sdk/java/src/main/java/ai/agentspan/enums/Position.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/Position.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.enums; +package org.conductoross.conductor.ai.enums; import com.fasterxml.jackson.annotation.JsonProperty; @@ -17,8 +17,10 @@ public enum Position { public String toJsonValue() { try { - return Position.class.getField(name()) - .getAnnotation(JsonProperty.class).value(); + return Position.class + .getField(name()) + .getAnnotation(JsonProperty.class) + .value(); } catch (NoSuchFieldException e) { return name().toLowerCase(); } diff --git a/sdk/java/src/main/java/ai/agentspan/enums/Strategy.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/Strategy.java similarity index 80% rename from sdk/java/src/main/java/ai/agentspan/enums/Strategy.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/enums/Strategy.java index f44c474ae..c8f94fdab 100644 --- a/sdk/java/src/main/java/ai/agentspan/enums/Strategy.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/enums/Strategy.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.enums; +package org.conductoross.conductor.ai.enums; import com.fasterxml.jackson.annotation.JsonProperty; @@ -38,8 +38,10 @@ public enum Strategy { public String toJsonValue() { try { - return Strategy.class.getField(name()) - .getAnnotation(JsonProperty.class).value(); + return Strategy.class + .getField(name()) + .getAnnotation(JsonProperty.class) + .value(); } catch (NoSuchFieldException e) { return name().toLowerCase(); } diff --git a/sdk/java/src/main/java/ai/agentspan/exceptions/AgentAPIException.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/AgentAPIException.java similarity index 93% rename from sdk/java/src/main/java/ai/agentspan/exceptions/AgentAPIException.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/AgentAPIException.java index 9ca2f385f..cd38ac46d 100644 --- a/sdk/java/src/main/java/ai/agentspan/exceptions/AgentAPIException.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/AgentAPIException.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.exceptions; +package org.conductoross.conductor.ai.exceptions; /** * Thrown when the Agentspan server returns a non-2xx HTTP response. diff --git a/sdk/java/src/main/java/ai/agentspan/exceptions/AgentNotFoundException.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/AgentNotFoundException.java similarity index 92% rename from sdk/java/src/main/java/ai/agentspan/exceptions/AgentNotFoundException.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/AgentNotFoundException.java index 727937fcd..8b6a93052 100644 --- a/sdk/java/src/main/java/ai/agentspan/exceptions/AgentNotFoundException.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/AgentNotFoundException.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.exceptions; +package org.conductoross.conductor.ai.exceptions; /** * Thrown when the workflow / agent / execution ID is not found (HTTP 404). diff --git a/sdk/java/src/main/java/ai/agentspan/exceptions/AgentspanException.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/AgentspanException.java similarity index 89% rename from sdk/java/src/main/java/ai/agentspan/exceptions/AgentspanException.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/AgentspanException.java index bb8d0d75c..67049da62 100644 --- a/sdk/java/src/main/java/ai/agentspan/exceptions/AgentspanException.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/AgentspanException.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.exceptions; +package org.conductoross.conductor.ai.exceptions; /** * Base exception for all Agentspan SDK errors. diff --git a/sdk/java/src/main/java/ai/agentspan/exceptions/CredentialAuthException.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/CredentialAuthException.java similarity index 91% rename from sdk/java/src/main/java/ai/agentspan/exceptions/CredentialAuthException.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/CredentialAuthException.java index 321b69db3..caea7f8f1 100644 --- a/sdk/java/src/main/java/ai/agentspan/exceptions/CredentialAuthException.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/CredentialAuthException.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.exceptions; +package org.conductoross.conductor.ai.exceptions; /** * Execution token rejected by {@code POST /api/workers/secrets} (HTTP 401). diff --git a/sdk/java/src/main/java/ai/agentspan/exceptions/CredentialNotFoundException.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/CredentialNotFoundException.java similarity index 95% rename from sdk/java/src/main/java/ai/agentspan/exceptions/CredentialNotFoundException.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/CredentialNotFoundException.java index 999befd76..f9d5838ba 100644 --- a/sdk/java/src/main/java/ai/agentspan/exceptions/CredentialNotFoundException.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/CredentialNotFoundException.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.exceptions; +package org.conductoross.conductor.ai.exceptions; import java.util.List; diff --git a/sdk/java/src/main/java/ai/agentspan/exceptions/CredentialRateLimitException.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/CredentialRateLimitException.java similarity index 92% rename from sdk/java/src/main/java/ai/agentspan/exceptions/CredentialRateLimitException.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/CredentialRateLimitException.java index 9cad8c74a..0f84fcf7d 100644 --- a/sdk/java/src/main/java/ai/agentspan/exceptions/CredentialRateLimitException.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/CredentialRateLimitException.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.exceptions; +package org.conductoross.conductor.ai.exceptions; /** * Rate limit hit on {@code POST /api/workers/secrets} (HTTP 429). diff --git a/sdk/java/src/main/java/ai/agentspan/exceptions/CredentialServiceException.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/CredentialServiceException.java similarity index 93% rename from sdk/java/src/main/java/ai/agentspan/exceptions/CredentialServiceException.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/CredentialServiceException.java index ea0b2887b..09be72a9d 100644 --- a/sdk/java/src/main/java/ai/agentspan/exceptions/CredentialServiceException.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/exceptions/CredentialServiceException.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.exceptions; +package org.conductoross.conductor.ai.exceptions; /** * Credential resolution service returned 5xx or was unreachable. diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/execution/CliCommandExecutor.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/execution/CliCommandExecutor.java new file mode 100644 index 000000000..e339fdc59 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/execution/CliCommandExecutor.java @@ -0,0 +1,284 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.execution; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * Local executor for the auto-injected {@code run_command} CLI tool. + * + *

    Mirrors the Python ({@code cli_config.py}), TypeScript ({@code cli-config.ts}) + * and C# ({@code CliTool}) implementations: tokenize the command line, validate + * the executable against the whitelist, then run it via {@link ProcessBuilder}. + * + *

    LLMs routinely pack the whole command line into the {@code command} field + * (e.g. {@code "gh repo list --limit 5"}). {@link #tokenize(String)} splits it so + * validation keys off the executable and execution gets a proper argv. + * + *

    Failures (whitelist rejection, timeout, command-not-found, non-zero exit) + * are returned as an error result map rather than thrown — matching the Java + * code-execution worker and the C# {@code CliTool}. + */ +public final class CliCommandExecutor { + + private CliCommandExecutor() {} + + // ── Tokenization ────────────────────────────────────────────────────── + + /** + * Tokenize a command line into argv, honoring single and double quotes. + * Falls back to plain whitespace splitting if quotes are unbalanced. + */ + public static List tokenize(String command) { + List tokens = new ArrayList<>(); + if (command == null) return tokens; + + StringBuilder current = new StringBuilder(); + boolean hasCurrent = false; + char quote = 0; + + for (int i = 0; i < command.length(); i++) { + char ch = command.charAt(i); + if (quote != 0) { + if (ch == quote) { + quote = 0; + } else { + current.append(ch); + } + } else if (ch == '"' || ch == '\'') { + quote = ch; + hasCurrent = true; + } else if (Character.isWhitespace(ch)) { + if (hasCurrent) { + tokens.add(current.toString()); + current.setLength(0); + hasCurrent = false; + } + } else { + current.append(ch); + hasCurrent = true; + } + } + + if (quote != 0) { + // Unbalanced quotes — fall back to naive whitespace split. + tokens.clear(); + for (String t : command.trim().split("\\s+")) { + if (!t.isEmpty()) tokens.add(t); + } + return tokens; + } + if (hasCurrent) tokens.add(current.toString()); + return tokens; + } + + // ── Validation ──────────────────────────────────────────────────────── + + /** Basename of the executable (strip {@code /usr/bin/git} → {@code git}). */ + private static String basename(String exe) { + int idx = Math.max(exe.lastIndexOf('/'), exe.lastIndexOf('\\')); + return idx >= 0 ? exe.substring(idx + 1) : exe; + } + + /** + * Validate the executable against the whitelist. Empty whitelist permits all. + * + * @throws IllegalArgumentException if the executable is not in the whitelist + */ + static void validate(String executable, List allowedCommands) { + if (allowedCommands == null || allowedCommands.isEmpty()) { + return; // no restrictions + } + String base = basename(executable); + if (!allowedCommands.contains(base)) { + List sorted = new ArrayList<>(allowedCommands); + Collections.sort(sorted); + throw new IllegalArgumentException( + "Command '" + base + "' is not allowed. Allowed commands: " + String.join(", ", sorted)); + } + } + + // ── Execution ───────────────────────────────────────────────────────── + + /** Convenience overload: extract fields from the worker input map. */ + public static Map run(Map input, CliConfig config) { + String command = asString(input.get("command")); + List args = asStringList(input.get("args")); + String cwd = asString(input.get("cwd")); + boolean shell = Boolean.TRUE.equals(input.get("shell")); + return run( + command, + args, + cwd, + shell, + config.getAllowedCommands(), + config.getTimeout(), + config.getWorkingDir(), + config.isAllowShell()); + } + + /** + * Execute a command. Returns a map with {@code status}, {@code exit_code}, + * {@code stdout} and {@code stderr}. + */ + public static Map run( + String command, + List args, + String cwd, + boolean shell, + List allowedCommands, + int timeout, + String workingDir, + boolean allowShell) { + + if (command == null || command.isBlank()) { + return error("No command provided."); + } + + // Models frequently pass the entire command line as `command` + // (e.g. "gh repo list --limit 5") rather than splitting executable/args. + List tokens = tokenize(command); + if (tokens.isEmpty()) { + return error("No command provided."); + } + String executable = tokens.get(0); + + // Validate against whitelist (on the executable). + try { + validate(executable, allowedCommands); + } catch (IllegalArgumentException e) { + return error(e.getMessage()); + } + + // Shell gate. + if (shell && !allowShell) { + return error("Shell mode is disabled for this agent. Do not set shell=true."); + } + + int effectiveTimeout = timeout > 0 ? timeout : 30; + + // Merge any args embedded in the command line with the explicit args list. + List argv = new ArrayList<>(tokens.subList(1, tokens.size())); + if (args != null) { + for (String a : args) argv.add(a); + } + + String effectiveCwd = (cwd != null && !cwd.isEmpty()) + ? cwd + : (workingDir != null && !workingDir.isEmpty() ? workingDir : null); + + // Build the process command. + List fullCmd = new ArrayList<>(); + if (shell) { + StringBuilder cmdStr = new StringBuilder(shellQuote(executable)); + for (String a : argv) cmdStr.append(' ').append(shellQuote(a)); + if (isWindows()) { + fullCmd.add("cmd.exe"); + fullCmd.add("/c"); + } else { + fullCmd.add("/bin/sh"); + fullCmd.add("-c"); + } + fullCmd.add(cmdStr.toString()); + } else { + fullCmd.add(executable); + fullCmd.addAll(argv); + } + + ProcessBuilder pb = new ProcessBuilder(fullCmd); + if (effectiveCwd != null) pb.directory(new File(effectiveCwd)); + + ExecutorService pool = Executors.newFixedThreadPool(2); + try { + Process process; + try { + process = pb.start(); + } catch (IOException e) { + // ProcessBuilder.start throws IOException when the executable + // cannot be found (e.g. "error=2, No such file or directory"). + return error("Command not found: " + executable); + } + + Future stdoutF = pool.submit(() -> readStream(process.getInputStream())); + Future stderrF = pool.submit(() -> readStream(process.getErrorStream())); + + boolean finished = process.waitFor(effectiveTimeout, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + return error("Command timed out after " + effectiveTimeout + "s"); + } + + String stdout = stdoutF.get(2, TimeUnit.SECONDS); + String stderr = stderrF.get(2, TimeUnit.SECONDS); + int code = process.exitValue(); + + Map result = new LinkedHashMap<>(); + result.put("status", code == 0 ? "success" : "error"); + result.put("exit_code", code); + result.put("stdout", stdout); + result.put("stderr", stderr); + return result; + } catch (Exception e) { + return error(e.getMessage() != null ? e.getMessage() : e.toString()); + } finally { + pool.shutdownNow(); + } + } + + // ── Helpers ─────────────────────────────────────────────────────────── + + private static Map error(String stderr) { + Map m = new LinkedHashMap<>(); + m.put("status", "error"); + m.put("stdout", ""); + m.put("stderr", stderr); + return m; + } + + private static String readStream(InputStream is) { + try { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (Exception e) { + return ""; + } + } + + /** Shell-quote a token (mirrors Python {@code shlex.quote}). */ + private static String shellQuote(String s) { + if (s.isEmpty()) return "''"; + // Safe characters need no quoting. + if (s.matches("[a-zA-Z0-9_@%+=:,./-]+")) return s; + return "'" + s.replace("'", "'\\''") + "'"; + } + + private static boolean isWindows() { + return System.getProperty("os.name", "").toLowerCase().contains("win"); + } + + private static String asString(Object v) { + return v instanceof String ? (String) v : null; + } + + private static List asStringList(Object v) { + if (v == null) return Collections.emptyList(); + if (v instanceof List list) { + List out = new ArrayList<>(list.size()); + for (Object o : list) out.add(String.valueOf(o)); + return out; + } + return List.of(String.valueOf(v)); + } +} diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/execution/CliConfig.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/execution/CliConfig.java new file mode 100644 index 000000000..ca0e1bc0d --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/execution/CliConfig.java @@ -0,0 +1,105 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.execution; + +import java.util.ArrayList; +import java.util.List; + +/** + * Configuration for first-class CLI command execution on an Agent. + * + *

    When set on an agent, a {@code {name}_run_command} worker tool is injected + * automatically (see {@code AgentConfigSerializer} and {@code AgentRuntime.prepareWorkers}), + * allowing the LLM to execute shell commands locally within the configured + * constraints. The command is executed by {@link CliCommandExecutor}. + * + *

    {@code
    + * Agent agent = Agent.builder()
    + *     .name("ops")
    + *     .model("openai/gpt-4o")
    + *     .cliConfig(new CliConfig.Builder()
    + *         .allowedCommands(List.of("git", "gh", "curl"))
    + *         .timeout(60)
    + *         .build())
    + *     .build();
    + * }
    + */ +public class CliConfig { + + private final boolean enabled; + private final List allowedCommands; + private final int timeout; + private final String workingDir; + private final boolean allowShell; + + private CliConfig(Builder builder) { + this.enabled = builder.enabled; + this.allowedCommands = + builder.allowedCommands != null ? new ArrayList<>(builder.allowedCommands) : new ArrayList<>(); + this.timeout = builder.timeout > 0 ? builder.timeout : 30; + this.workingDir = builder.workingDir; + this.allowShell = builder.allowShell; + } + + public boolean isEnabled() { + return enabled; + } + + public List getAllowedCommands() { + return allowedCommands; + } + + public int getTimeout() { + return timeout; + } + + public String getWorkingDir() { + return workingDir; + } + + public boolean isAllowShell() { + return allowShell; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private boolean enabled = true; + private List allowedCommands; + private int timeout = 30; + private String workingDir; + private boolean allowShell = false; + + public Builder enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public Builder allowedCommands(List allowedCommands) { + this.allowedCommands = allowedCommands; + return this; + } + + public Builder timeout(int timeout) { + this.timeout = timeout; + return this; + } + + public Builder workingDir(String workingDir) { + this.workingDir = workingDir; + return this; + } + + public Builder allowShell(boolean allowShell) { + this.allowShell = allowShell; + return this; + } + + public CliConfig build() { + return new CliConfig(this); + } + } +} diff --git a/sdk/java/src/main/java/ai/agentspan/execution/CodeExecutor.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/execution/CodeExecutor.java similarity index 70% rename from sdk/java/src/main/java/ai/agentspan/execution/CodeExecutor.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/execution/CodeExecutor.java index 90c2e95bf..b4f644645 100644 --- a/sdk/java/src/main/java/ai/agentspan/execution/CodeExecutor.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/execution/CodeExecutor.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.execution; - -import ai.agentspan.model.ToolDef; +package org.conductoross.conductor.ai.execution; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.conductoross.conductor.ai.model.ToolDef; + /** * Base class for code execution environments. * @@ -43,8 +43,8 @@ public ToolDef asTool() { public ToolDef asTool(String name, String description) { if (name == null) name = "execute_code"; if (description == null) { - description = "Execute " + language + " code. Returns stdout, stderr, and exit code. " - + "Timeout: " + timeout + "s."; + description = "Execute " + language + " code. Returns stdout, stderr, and exit code. " + "Timeout: " + + timeout + "s."; } Map codeProp = new LinkedHashMap<>(); codeProp.put("type", "string"); @@ -58,6 +58,10 @@ public ToolDef asTool(String name, String description) { return ToolDef.builder() .name(name) .description(description) + // Propagate the executor's timeout so worker registration can size + // the Conductor task def's responseTimeout to the handler's blocking + // duration (avoids mid-flight re-dispatch of long-running execs). + .timeoutSeconds(timeout) .inputSchema(schema) .func(input -> { String code = (String) input.get("code"); @@ -66,17 +70,24 @@ public ToolDef asTool(String name, String description) { } ExecutionResult result = execute(code); return Map.of( - "status", result.isSuccess() ? "success" : "error", - "output", result.getOutput(), - "error", result.getError(), - "exitCode", result.getExitCode(), - "timedOut", result.isTimedOut() - ); + "status", result.isSuccess() ? "success" : "error", + "output", result.getOutput(), + "error", result.getError(), + "exitCode", result.getExitCode(), + "timedOut", result.isTimedOut()); }) .build(); } - public String getLanguage() { return language; } - public int getTimeout() { return timeout; } - public String getWorkingDir() { return workingDir; } + public String getLanguage() { + return language; + } + + public int getTimeout() { + return timeout; + } + + public String getWorkingDir() { + return workingDir; + } } diff --git a/sdk/java/src/main/java/ai/agentspan/execution/DockerCodeExecutor.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/execution/DockerCodeExecutor.java similarity index 81% rename from sdk/java/src/main/java/ai/agentspan/execution/DockerCodeExecutor.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/execution/DockerCodeExecutor.java index 574edf33c..2af209faf 100644 --- a/sdk/java/src/main/java/ai/agentspan/execution/DockerCodeExecutor.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/execution/DockerCodeExecutor.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.execution; +package org.conductoross.conductor.ai.execution; import java.io.IOException; import java.nio.file.Files; @@ -37,7 +37,9 @@ public DockerCodeExecutor(String image, String language, int timeout, String wor this.image = image; } - public String getImage() { return image; } + public String getImage() { + return image; + } @Override public ExecutionResult execute(String code) { @@ -51,14 +53,20 @@ public ExecutionResult execute(String code) { String interpreter = getInterpreter(language); List command = new ArrayList<>(List.of( - "docker", "run", "--rm", - "-v", tempFile.toAbsolutePath() + ":" + containerPath + ":ro", - "--memory", "256m", - "--cpus", "0.5", - "--network", "none", - image, - interpreter, containerPath - )); + "docker", + "run", + "--rm", + "-v", + tempFile.toAbsolutePath() + ":" + containerPath + ":ro", + "--memory", + "256m", + "--cpus", + "0.5", + "--network", + "none", + image, + interpreter, + containerPath)); ProcessBuilder pb = new ProcessBuilder(command); pb.redirectErrorStream(false); @@ -79,7 +87,10 @@ public ExecutionResult execute(String code) { return new ExecutionResult("", "Docker execution error: " + e.getMessage(), 1, false); } finally { if (tempFile != null) { - try { Files.deleteIfExists(tempFile); } catch (IOException ignored) {} + try { + Files.deleteIfExists(tempFile); + } catch (IOException ignored) { + } } } } diff --git a/sdk/java/src/main/java/ai/agentspan/execution/ExecutionResult.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/execution/ExecutionResult.java similarity index 69% rename from sdk/java/src/main/java/ai/agentspan/execution/ExecutionResult.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/execution/ExecutionResult.java index ebff4bebf..8ef2f2628 100644 --- a/sdk/java/src/main/java/ai/agentspan/execution/ExecutionResult.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/execution/ExecutionResult.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.execution; +package org.conductoross.conductor.ai.execution; /** The result of a code execution. */ public class ExecutionResult { @@ -19,17 +19,27 @@ public ExecutionResult(String output, String error, int exitCode, boolean timedO } /** Standard output from the execution. */ - public String getOutput() { return output; } + public String getOutput() { + return output; + } /** Standard error output (if any). */ - public String getError() { return error; } + public String getError() { + return error; + } /** Process exit code (0 = success). */ - public int getExitCode() { return exitCode; } + public int getExitCode() { + return exitCode; + } /** {@code true} if execution was killed due to timeout. */ - public boolean isTimedOut() { return timedOut; } + public boolean isTimedOut() { + return timedOut; + } /** {@code true} if the execution succeeded (exit code 0, no timeout). */ - public boolean isSuccess() { return exitCode == 0 && !timedOut; } + public boolean isSuccess() { + return exitCode == 0 && !timedOut; + } } diff --git a/sdk/java/src/main/java/ai/agentspan/frameworks/AdkBridge.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/frameworks/AdkBridge.java similarity index 86% rename from sdk/java/src/main/java/ai/agentspan/frameworks/AdkBridge.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/frameworks/AdkBridge.java index d09da20dc..2fbf82539 100644 --- a/sdk/java/src/main/java/ai/agentspan/frameworks/AdkBridge.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/frameworks/AdkBridge.java @@ -1,11 +1,21 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.frameworks; +package org.conductoross.conductor.ai.frameworks; -import ai.agentspan.Agent; -import ai.agentspan.CallbackHandler; -import ai.agentspan.model.ToolDef; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.CallbackHandler; +import org.conductoross.conductor.ai.model.ToolDef; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.adk.agents.BaseAgent; import com.google.adk.agents.Callbacks; @@ -23,21 +33,8 @@ import com.google.adk.tools.GoogleSearchTool; import com.google.genai.types.Content; import com.google.genai.types.FunctionDeclaration; -import com.google.genai.types.GenerateContentConfig; import com.google.genai.types.Part; import com.google.genai.types.Schema; -import com.google.genai.types.ThinkingConfig; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; /** * Adapter that takes a native Google ADK {@link BaseAgent} and produces an @@ -78,12 +75,12 @@ public final class AdkBridge { /** Map from ADK callback wire-field name to LlmAgent getter method. */ private static final String[][] CALLBACK_FIELDS = { - {"before_agent_callback", "beforeAgentCallback"}, - {"after_agent_callback", "afterAgentCallback"}, - {"before_model_callback", "beforeModelCallback"}, - {"after_model_callback", "afterModelCallback"}, - {"before_tool_callback", "beforeToolCallback"}, - {"after_tool_callback", "afterToolCallback"}, + {"before_agent_callback", "beforeAgentCallback"}, + {"after_agent_callback", "afterAgentCallback"}, + {"before_model_callback", "beforeModelCallback"}, + {"after_model_callback", "afterModelCallback"}, + {"before_tool_callback", "beforeToolCallback"}, + {"after_tool_callback", "afterToolCallback"}, }; private AdkBridge() {} @@ -93,7 +90,7 @@ private AdkBridge() {} /** * Convert any native ADK {@link BaseAgent} ({@code LlmAgent}, * {@code SequentialAgent}, {@code ParallelAgent}, {@code LoopAgent}, …) - * into an Agentspan {@link Agent} ready for {@code Agentspan.run(...)}. + * into an Agentspan {@link Agent} ready for {@code runtime.run(...)}. */ public static Agent toAgentspan(BaseAgent adk) { return agentBuilder(adk).build(); @@ -109,7 +106,7 @@ public static Agent toAgentspan(BaseAgent adk) { * Agent decorated = AdkBridge.agentBuilder(llmAgent) * .guardrails(piiGuard) * .build(); - * Agentspan.run(decorated, "..."); + * new AgentRuntime().run(decorated, "..."); * }
    */ public static Agent.Builder agentBuilder(BaseAgent adk) { @@ -126,13 +123,10 @@ private static Agent toAgentspan(BaseAgent adk, java.util.IdentityHashMap visited) { if (visited.putIfAbsent(adk, Boolean.TRUE) != null) { throw new IllegalArgumentException( - "AdkBridge: cycle detected in subAgents/AgentTool graph at agent '" - + adk.name() + "'"); + "AdkBridge: cycle detected in subAgents/AgentTool graph at agent '" + adk.name() + "'"); } - Agent.Builder b = Agent.builder() - .name(adk.name()) - .framework("google_adk"); + Agent.Builder b = Agent.builder().name(adk.name()).framework("google_adk"); // Model + instruction live at the Agentspan top level so the // worker poller / debug tools see them directly. Everything else @@ -167,8 +161,8 @@ private static Agent.Builder agentBuilder(BaseAgent adk, java.util.IdentityHashM if (handler != null) b.callbacks(handler); } - Map frameworkConfig = buildRawConfig(adk, /*topLevel=*/ true, - new java.util.IdentityHashMap<>()); + Map frameworkConfig = + buildRawConfig(adk, /*topLevel=*/ true, new java.util.IdentityHashMap<>()); // Strip the simple scalars already set on the Agent.Builder — the // serializer emits them at the top level and frameworkConfig.putAll // would just overwrite with the same value. @@ -194,12 +188,10 @@ private static Agent.Builder agentBuilder(BaseAgent adk, java.util.IdentityHashM * graph that would otherwise blow the stack. */ private static Map buildRawConfig( - BaseAgent adk, boolean topLevel, - java.util.IdentityHashMap visited) { + BaseAgent adk, boolean topLevel, java.util.IdentityHashMap visited) { if (visited.putIfAbsent(adk, Boolean.TRUE) != null) { throw new IllegalArgumentException( - "AdkBridge: cycle detected in subAgents/AgentTool graph at agent '" - + adk.name() + "'"); + "AdkBridge: cycle detected in subAgents/AgentTool graph at agent '" + adk.name() + "'"); } Map raw = new LinkedHashMap<>(); @@ -212,9 +204,7 @@ private static Map buildRawConfig( // LoopAgent). Server reads `_type` to set strategy; without this the // normalizer defaults to "handoff" and our pipelines run wrong. String typeName = adk.getClass().getSimpleName(); - if ("SequentialAgent".equals(typeName) - || "ParallelAgent".equals(typeName) - || "LoopAgent".equals(typeName)) { + if ("SequentialAgent".equals(typeName) || "ParallelAgent".equals(typeName) || "LoopAgent".equals(typeName)) { raw.put("_type", typeName); } @@ -252,7 +242,7 @@ private static Map buildRawConfig( // Transfer restrictions (consumed by parent normalizer when this // agent appears as a sub_agent — see GoogleADKNormalizer line ~134) if (llm.disallowTransferToParent()) raw.put("disallow_transfer_to_parent", true); - if (llm.disallowTransferToPeers()) raw.put("disallow_transfer_to_peers", true); + if (llm.disallowTransferToPeers()) raw.put("disallow_transfer_to_peers", true); // GenerateContentConfig → server's `generate_content_config` llm.generateContentConfig().ifPresent(gc -> { @@ -321,9 +311,11 @@ private static String extractInstruction(Instruction inst) { // any failure but never break the run. return p.getInstruction().apply(null).blockingGet(); } catch (Throwable t) { - log.warn("AdkBridge: Instruction.Provider for '{}' threw during static " - + "resolution; falling back to empty instruction. {}", - t.getClass().getSimpleName(), t.getMessage()); + log.warn( + "AdkBridge: Instruction.Provider for '{}' threw during static " + + "resolution; falling back to empty instruction. {}", + t.getClass().getSimpleName(), + t.getMessage()); return null; } } @@ -365,8 +357,7 @@ private static List> buildToolMaps( } private static void addToolMap( - BaseTool t, List> out, - java.util.IdentityHashMap visited) { + BaseTool t, List> out, java.util.IdentityHashMap visited) { if (t instanceof FunctionTool ft) { Map m = new LinkedHashMap<>(); m.put("_worker_ref", ft.name()); @@ -407,33 +398,38 @@ private static void addToolMap( addToolMap(inner, out, visited); } } catch (Throwable th) { - log.error("AdkBridge: BaseToolset '{}' expansion failed; tools from this " - + "toolset will NOT be available to the agent. Cause: {}", - t.getClass().getName(), th.toString()); + log.error( + "AdkBridge: BaseToolset '{}' expansion failed; tools from this " + + "toolset will NOT be available to the agent. Cause: {}", + t.getClass().getName(), + th.toString()); } finally { - try { bts.close(); } - catch (Throwable th) { + try { + bts.close(); + } catch (Throwable th) { log.debug("AdkBridge: BaseToolset.close() threw: {}", th.toString()); } } } else { - log.warn("AdkBridge: dropping unsupported BaseTool subclass '{}'", t.getClass().getName()); + log.warn( + "AdkBridge: dropping unsupported BaseTool subclass '{}'", + t.getClass().getName()); } } private static ToolDef toToolDef(BaseTool t, java.util.IdentityHashMap visited) { if (t instanceof FunctionTool ft) return functionToolToDef(ft); - if (t instanceof AgentTool at) return agentToolToDef(at, visited); + if (t instanceof AgentTool at) return agentToolToDef(at, visited); // GoogleSearchTool / BuiltInCodeExecutionTool / BaseToolset: server-side // builtin tools that don't need a local worker. Returning null is // intentional — extractTopLevelTools drops nulls and these still get // emitted into the wire format via buildToolMaps (frameworkConfig.tools). - if (t instanceof GoogleSearchTool - || t instanceof BuiltInCodeExecutionTool - || t instanceof BaseToolset) { + if (t instanceof GoogleSearchTool || t instanceof BuiltInCodeExecutionTool || t instanceof BaseToolset) { return null; } - log.warn("AdkBridge: dropping unsupported BaseTool subclass '{}'", t.getClass().getName()); + log.warn( + "AdkBridge: dropping unsupported BaseTool subclass '{}'", + t.getClass().getName()); return null; } @@ -455,12 +451,13 @@ private static ToolDef functionToolToDef(FunctionTool ft) { // double-wrapped stack trace. Throwable cause = ite.getCause() != null ? ite.getCause() : ite; if (cause instanceof RuntimeException re) throw re; - throw new RuntimeException("ADK FunctionTool '" + name + "' threw: " - + cause.getMessage(), cause); + throw new RuntimeException("ADK FunctionTool '" + name + "' threw: " + cause.getMessage(), cause); } catch (IllegalAccessException | IllegalArgumentException ex) { - throw new RuntimeException("ADK FunctionTool '" + name - + "' invocation failed (check parameter types and the -parameters " - + "compiler flag): " + ex.getMessage(), ex); + throw new RuntimeException( + "ADK FunctionTool '" + name + + "' invocation failed (check parameter types and the -parameters " + + "compiler flag): " + ex.getMessage(), + ex); } }; @@ -513,8 +510,7 @@ private static Map buildInputSchema(FunctionTool ft) { } } } catch (Throwable t) { - log.debug("AdkBridge: FunctionDeclaration parse failed for {}: {}", - ft.name(), t.getMessage()); + log.debug("AdkBridge: FunctionDeclaration parse failed for {}: {}", ft.name(), t.getMessage()); } // Reflection fallback when the declaration doesn't expose properties. @@ -542,8 +538,7 @@ private static Map buildInputSchema(FunctionTool ft) { * server invoked the tool with the schema name. Honor {@code @Schema.name} * first. */ - private static final java.util.Set WARNED_ARG_METHODS = - java.util.concurrent.ConcurrentHashMap.newKeySet(); + private static final java.util.Set WARNED_ARG_METHODS = java.util.concurrent.ConcurrentHashMap.newKeySet(); private static String paramName(Parameter p) { Annotations.Schema ann = p.getAnnotation(Annotations.Schema.class); @@ -557,13 +552,16 @@ private static String paramName(Parameter p) { // Warn loudly once per method so the user notices and either adds // -parameters or switches to @Schema(name=...). if (name != null && name.matches("arg\\d+")) { - String key = p.getDeclaringExecutable().getDeclaringClass().getName() - + "#" + p.getDeclaringExecutable().getName(); + String key = p.getDeclaringExecutable().getDeclaringClass().getName() + "#" + + p.getDeclaringExecutable().getName(); if (WARNED_ARG_METHODS.add(key)) { - log.warn("AdkBridge: method '{}' parameter names are not preserved " - + "(got '{}'). The LLM will see meaningless parameter names. " - + "Compile with javac -parameters, or use " - + "@Schema(name=\"...\") on each parameter.", key, name); + log.warn( + "AdkBridge: method '{}' parameter names are not preserved " + + "(got '{}'). The LLM will see meaningless parameter names. " + + "Compile with javac -parameters, or use " + + "@Schema(name=\"...\") on each parameter.", + key, + name); } } return name; @@ -597,10 +595,8 @@ private static Map schemaToMap(Schema s) { private static String jsonTypeOf(Class type) { if (type == String.class) return "string"; - if (type == int.class || type == Integer.class - || type == long.class || type == Long.class) return "integer"; - if (type == double.class || type == Double.class - || type == float.class || type == Float.class) return "number"; + if (type == int.class || type == Integer.class || type == long.class || type == Long.class) return "integer"; + if (type == double.class || type == Double.class || type == float.class || type == Float.class) return "number"; if (type == boolean.class || type == Boolean.class) return "boolean"; if (type.isArray() || List.class.isAssignableFrom(type)) return "array"; return "object"; @@ -618,7 +614,7 @@ private static Object[] buildArgs(Method method, Map inputData) // java.time.*, enums, Optional, List, Map, arrays via Jackson // and the generic type. Keeps the bridge in lockstep with the // @Tool fix from #236. - args[i] = ai.agentspan.internal.ToolRegistry.coerceArgument( + args[i] = org.conductoross.conductor.ai.internal.ToolRegistry.coerceArgument( raw, params[i].getType(), params[i].getParameterizedType()); } return args; @@ -667,20 +663,24 @@ private static boolean callbackListIsNonEmpty(LlmAgent llm, String getter) { @SuppressWarnings("unchecked") private static CallbackHandler wrapCallbacks(LlmAgent llm) { List beforeAgent = callbackList(llm, "beforeAgentCallback"); - List afterAgent = callbackList(llm, "afterAgentCallback"); + List afterAgent = callbackList(llm, "afterAgentCallback"); List beforeModel = callbackList(llm, "beforeModelCallback"); - List afterModel = callbackList(llm, "afterModelCallback"); - List beforeTool = callbackList(llm, "beforeToolCallback"); - List afterTool = callbackList(llm, "afterToolCallback"); - - if (beforeAgent.isEmpty() && afterAgent.isEmpty() - && beforeModel.isEmpty() && afterModel.isEmpty() - && beforeTool.isEmpty() && afterTool.isEmpty()) { + List afterModel = callbackList(llm, "afterModelCallback"); + List beforeTool = callbackList(llm, "beforeToolCallback"); + List afterTool = callbackList(llm, "afterToolCallback"); + + if (beforeAgent.isEmpty() + && afterAgent.isEmpty() + && beforeModel.isEmpty() + && afterModel.isEmpty() + && beforeTool.isEmpty() + && afterTool.isEmpty()) { return null; } return new CallbackHandler() { - @Override public Map onAgentStart(Map in) { + @Override + public Map onAgentStart(Map in) { for (var cb : beforeAgent) { try { Content out = cb.call(null).blockingGet(); @@ -691,7 +691,9 @@ private static CallbackHandler wrapCallbacks(LlmAgent llm) { } return Map.of(); } - @Override public Map onAgentEnd(Map in) { + + @Override + public Map onAgentEnd(Map in) { for (var cb : afterAgent) { try { Content out = cb.call(null).blockingGet(); @@ -702,13 +704,17 @@ private static CallbackHandler wrapCallbacks(LlmAgent llm) { } return Map.of(); } - @Override public Map onModelStart(Map in) { + + @Override + public Map onModelStart(Map in) { LlmRequest.Builder req = reconstructLlmRequest(in); for (var cb : beforeModel) { try { LlmResponse resp = cb.call(null, req).blockingGet(); if (resp != null) { - return Map.of("content", resp.content().map(AdkBridge::textOf).orElse("")); + return Map.of( + "content", + resp.content().map(AdkBridge::textOf).orElse("")); } } catch (Throwable t) { log.warn("ADK beforeModelCallback failed: {}", t.getMessage()); @@ -716,13 +722,17 @@ private static CallbackHandler wrapCallbacks(LlmAgent llm) { } return Map.of(); } - @Override public Map onModelEnd(Map in) { + + @Override + public Map onModelEnd(Map in) { LlmResponse resp = reconstructLlmResponse(in); for (var cb : afterModel) { try { LlmResponse rewritten = cb.call(null, resp).blockingGet(); if (rewritten != null) { - return Map.of("content", rewritten.content().map(AdkBridge::textOf).orElse("")); + return Map.of( + "content", + rewritten.content().map(AdkBridge::textOf).orElse("")); } } catch (Throwable t) { log.warn("ADK afterModelCallback failed: {}", t.getMessage()); @@ -730,14 +740,17 @@ private static CallbackHandler wrapCallbacks(LlmAgent llm) { } return Map.of(); } - @Override public Map onToolStart(Map in) { + + @Override + public Map onToolStart(Map in) { String toolName = (String) in.getOrDefault("tool_name", ""); Map args = (Map) in.getOrDefault("args", Map.of()); for (var cb : beforeTool) { try { // BaseTool/ToolContext are null — user callbacks should // base decisions on the args/toolName they get here. - Map out = cb.call(null, null, args, null).blockingGet(); + Map out = + cb.call(null, null, args, null).blockingGet(); if (out != null) return out; } catch (Throwable t) { log.warn("ADK beforeToolCallback failed for '{}': {}", toolName, t.getMessage()); @@ -745,13 +758,16 @@ private static CallbackHandler wrapCallbacks(LlmAgent llm) { } return Map.of(); } - @Override public Map onToolEnd(Map in) { + + @Override + public Map onToolEnd(Map in) { String toolName = (String) in.getOrDefault("tool_name", ""); Map args = (Map) in.getOrDefault("args", Map.of()); Object result = in.get("result"); for (var cb : afterTool) { try { - Map out = cb.call(null, null, args, null, result).blockingGet(); + Map out = + cb.call(null, null, args, null, result).blockingGet(); if (out != null) return out; } catch (Throwable t) { log.warn("ADK afterToolCallback failed for '{}': {}", toolName, t.getMessage()); @@ -780,7 +796,8 @@ private static List callbackList(LlmAgent llm, String getter) { v = opt.get(); } if (v instanceof List list) return (List) list; - } catch (Throwable ignored) {} + } catch (Throwable ignored) { + } return List.of(); } @@ -844,8 +861,7 @@ private static LlmResponse reconstructLlmResponse(Map in) { Method build = b.getClass().getMethod("build"); return (LlmResponse) build.invoke(b); } catch (Throwable t) { - log.debug("AdkBridge: LlmResponse reconstruction failed, returning null. {}", - t.getMessage()); + log.debug("AdkBridge: LlmResponse reconstruction failed, returning null. {}", t.getMessage()); return null; } } diff --git a/sdk/java/src/main/java/ai/agentspan/frameworks/LangChain4jAgent.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/frameworks/LangChain4jAgent.java similarity index 86% rename from sdk/java/src/main/java/ai/agentspan/frameworks/LangChain4jAgent.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/frameworks/LangChain4jAgent.java index 405e31d0b..ceebcbf95 100644 --- a/sdk/java/src/main/java/ai/agentspan/frameworks/LangChain4jAgent.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/frameworks/LangChain4jAgent.java @@ -1,11 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.frameworks; - -import ai.agentspan.Agent; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.ToolDef; +package org.conductoross.conductor.ai.frameworks; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @@ -15,6 +11,10 @@ import java.util.Map; import java.util.function.Function; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.ToolDef; + /** * Bridges LangChain4j tool objects to Agentspan {@link Agent}. * @@ -53,14 +53,10 @@ private LangChain4jAgent() {} * @param instructions system prompt / instructions for the agent * @param toolObjects objects with {@code @dev.langchain4j.agent.tool.Tool} methods * @return an Agentspan Agent ready to pass to - * {@link ai.agentspan.AgentRuntime#plan(Agent)} or - * {@link ai.agentspan.AgentRuntime#run(Agent, String)} + * {@link org.conductoross.conductor.ai.AgentRuntime#plan(Agent)} or + * {@link org.conductoross.conductor.ai.AgentRuntime#run(Agent, String)} */ - public static Agent from( - String name, - String model, - String instructions, - Object... toolObjects) { + public static Agent from(String name, String model, String instructions, Object... toolObjects) { List tools = extractTools(toolObjects); @@ -133,12 +129,14 @@ static List extractTools(Object[] toolObjects) { // confusing double-wrap. Throwable cause = ite.getCause() != null ? ite.getCause() : ite; if (cause instanceof RuntimeException re) throw re; - throw new RuntimeException("LangChain4j tool '" + finalName - + "' threw: " + cause.getMessage(), cause); + throw new RuntimeException( + "LangChain4j tool '" + finalName + "' threw: " + cause.getMessage(), cause); } catch (IllegalAccessException | IllegalArgumentException ex) { - throw new RuntimeException("LangChain4j tool '" + finalName - + "' invocation failed (check parameter types and the " - + "-parameters compiler flag): " + ex.getMessage(), ex); + throw new RuntimeException( + "LangChain4j tool '" + finalName + + "' invocation failed (check parameter types and the " + + "-parameters compiler flag): " + ex.getMessage(), + ex); } }; @@ -187,7 +185,8 @@ private static String resolveToolName(Method method) { try { String name = (String) ann.annotationType().getMethod("name").invoke(ann); if (name != null && !name.isEmpty()) return name; - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } return method.getName(); } @@ -207,7 +206,8 @@ private static String resolveDescription(Method method) { return String.join(" ", parts); } } - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } return ""; } @@ -254,17 +254,18 @@ private static Map buildInputSchema(Method method) { * 2. Compiler-retained name (not "arg0") * 3. Positional fallback "arg{i}" */ - private static final java.util.Set WARNED_ARG_METHODS = - java.util.concurrent.ConcurrentHashMap.newKeySet(); + private static final java.util.Set WARNED_ARG_METHODS = java.util.concurrent.ConcurrentHashMap.newKeySet(); private static String resolveParamName(Parameter param, int index) { // Check @P first for (java.lang.annotation.Annotation ann : param.getAnnotations()) { if (ann.annotationType().getName().equals("dev.langchain4j.agent.tool.P")) { try { - String name = (String) ann.annotationType().getMethod("value").invoke(ann); + String name = + (String) ann.annotationType().getMethod("value").invoke(ann); if (name != null && !name.isEmpty()) return name; - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } } // Compiler-retained name @@ -275,14 +276,15 @@ private static String resolveParamName(Parameter param, int index) { // Compiler-retained names require javac -parameters at the user's // build time. Without it the LLM sees arg0/arg1 — guaranteed // garbage tool calls. Warn once per method so the user notices. - String key = param.getDeclaringExecutable().getDeclaringClass().getName() - + "#" + param.getDeclaringExecutable().getName(); + String key = param.getDeclaringExecutable().getDeclaringClass().getName() + "#" + + param.getDeclaringExecutable().getName(); if (WARNED_ARG_METHODS.add(key)) { - org.slf4j.LoggerFactory.getLogger(LangChain4jAgent.class).warn( - "LangChain4jAgent: method '{}' parameter names are not preserved. " - + "The LLM will see meaningless 'arg0' parameter names. Compile " - + "with javac -parameters or use @P(\"...\") on each parameter.", - key); + org.slf4j.LoggerFactory.getLogger(LangChain4jAgent.class) + .warn( + "LangChain4jAgent: method '{}' parameter names are not preserved. " + + "The LLM will see meaningless 'arg0' parameter names. Compile " + + "with javac -parameters or use @P(\"...\") on each parameter.", + key); } return "arg" + index; } @@ -305,7 +307,7 @@ private static Object[] buildMethodArgs(Method method, Map input // + java.time.* + enums + Optional + List/Map/arrays via Jackson. // Without this, declaring a LocalDate / List / enum param // on an @Tool method would IllegalArgumentException at invoke time. - args[i] = ai.agentspan.internal.ToolRegistry.coerceArgument( + args[i] = org.conductoross.conductor.ai.internal.ToolRegistry.coerceArgument( raw, param.getType(), param.getParameterizedType()); } return args; diff --git a/sdk/java/src/main/java/ai/agentspan/frameworks/LangChainBridge.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/frameworks/LangChainBridge.java similarity index 84% rename from sdk/java/src/main/java/ai/agentspan/frameworks/LangChainBridge.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/frameworks/LangChainBridge.java index 8f5096b45..33f88f913 100644 --- a/sdk/java/src/main/java/ai/agentspan/frameworks/LangChainBridge.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/frameworks/LangChainBridge.java @@ -1,9 +1,9 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.frameworks; +package org.conductoross.conductor.ai.frameworks; -import ai.agentspan.Agent; +import org.conductoross.conductor.ai.Agent; import dev.langchain4j.model.ModelProvider; import dev.langchain4j.model.chat.ChatModel; @@ -34,23 +34,19 @@ private LangChainBridge() {} * Agent agent = LangChainBridge.agentBuilder("name", model, "prompt", new MyTools()) * .guardrails(piiGuard) * .build(); - * Agentspan.run(agent, "..."); + * new AgentRuntime().run(agent, "..."); * }
    * *

    For the simple no-decoration case, prefer the direct drop-in: - * {@code Agentspan.run(model, prompt, tools)}. + * {@code runtime.run(model, prompt, tools)}. */ public static Agent.Builder agentBuilder(String name, ChatModel model, String systemPrompt, Object... tools) { String modelString = providerSlashModel(model); // Mirrors LangChain4jAgent.from but returns the Builder so callers can // decorate. Imports are kept package-local since LangChain4jAgent is - // an ai.agentspan.frameworks class. - java.util.List toolDefs = - LangChain4jAgent.extractTools(tools); - Agent.Builder b = Agent.builder() - .name(name) - .model(modelString) - .instructions(systemPrompt); + // an org.conductoross.conductor.ai.frameworks class. + java.util.List toolDefs = LangChain4jAgent.extractTools(tools); + Agent.Builder b = Agent.builder().name(name).model(modelString).instructions(systemPrompt); if (!toolDefs.isEmpty()) { b.tools(toolDefs); } @@ -69,11 +65,12 @@ public static String providerSlashModel(ChatModel model) { String modelName = null; try { modelName = model.defaultRequestParameters().modelName(); - } catch (Throwable ignored) {} + } catch (Throwable ignored) { + } if (modelName == null || modelName.isEmpty()) { - throw new IllegalArgumentException( - "Could not read model name from ChatModel " + model.getClass().getName()); + throw new IllegalArgumentException("Could not read model name from ChatModel " + + model.getClass().getName()); } // If the user already provided a slash-format string, accept it. diff --git a/sdk/java/src/main/java/ai/agentspan/frameworks/OpenAIAgent.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/frameworks/OpenAIAgent.java similarity index 80% rename from sdk/java/src/main/java/ai/agentspan/frameworks/OpenAIAgent.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/frameworks/OpenAIAgent.java index 75dc83b4e..122e16a34 100644 --- a/sdk/java/src/main/java/ai/agentspan/frameworks/OpenAIAgent.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/frameworks/OpenAIAgent.java @@ -1,11 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.frameworks; - -import ai.agentspan.Agent; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.ToolDef; +package org.conductoross.conductor.ai.frameworks; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @@ -15,6 +11,10 @@ import java.util.Map; import java.util.function.Function; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.ToolDef; + /** * Bridges the OpenAI Agents SDK shape to Agentspan {@link Agent}. * @@ -32,7 +32,7 @@ * .instructions("You are a helpful assistant") * .model("openai/gpt-4o") * .build(); - * Agentspan.run(agent, "Say hi"); + * new AgentRuntime().run(agent, "Say hi"); * } * *

    The server's {@code OpenAINormalizer} consumes the wire payload (the SDK @@ -60,9 +60,20 @@ public static final class Builder { private final List handoffs = new ArrayList<>(); private String outputType; - public Builder name(String name) { this.name = name; return this; } - public Builder model(String model) { this.model = model; return this; } - public Builder instructions(String instructions) { this.instructions = instructions; return this; } + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder model(String model) { + this.model = model; + return this; + } + + public Builder instructions(String instructions) { + this.instructions = instructions; + return this; + } /** Add @Tool-annotated POJO(s); each annotated method becomes an Agentspan worker tool. */ public Builder tools(Object... toolObjects) { @@ -77,15 +88,16 @@ public Builder handoffs(Agent... agents) { } /** Optional structured-output type name (the server hooks into its structured-output normalizer). */ - public Builder outputType(String typeName) { this.outputType = typeName; return this; } + public Builder outputType(String typeName) { + this.outputType = typeName; + return this; + } public Agent build() { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("OpenAIAgent.name is required"); } - Agent.Builder b = Agent.builder() - .name(name) - .framework("openai"); + Agent.Builder b = Agent.builder().name(name).framework("openai"); if (model != null && !model.isEmpty()) b.model(model); if (instructions != null && !instructions.isEmpty()) b.instructions(instructions); if (!tools.isEmpty()) b.tools(tools.toArray(new ToolDef[0])); @@ -145,12 +157,14 @@ private static List extractTools(Object[] toolObjects) { // confusing double-wrap. Throwable cause = ite.getCause() != null ? ite.getCause() : ite; if (cause instanceof RuntimeException re) throw re; - throw new RuntimeException("OpenAI tool '" + finalName + "' threw: " - + cause.getMessage(), cause); + throw new RuntimeException( + "OpenAI tool '" + finalName + "' threw: " + cause.getMessage(), cause); } catch (IllegalAccessException | IllegalArgumentException ex) { - throw new RuntimeException("OpenAI tool '" + finalName - + "' invocation failed (check parameter types and the " - + "-parameters compiler flag): " + ex.getMessage(), ex); + throw new RuntimeException( + "OpenAI tool '" + finalName + + "' invocation failed (check parameter types and the " + + "-parameters compiler flag): " + ex.getMessage(), + ex); } }; @@ -171,7 +185,7 @@ private static boolean isToolMethod(Method m) { for (java.lang.annotation.Annotation ann : m.getAnnotations()) { String name = ann.annotationType().getName(); if (name.equals("dev.langchain4j.agent.tool.Tool")) return true; - if (name.equals("ai.agentspan.annotations.Tool")) return true; + if (name.equals("org.conductoross.conductor.ai.annotations.Tool")) return true; } return false; } @@ -180,11 +194,13 @@ private static String resolveToolName(Method method) { for (java.lang.annotation.Annotation ann : method.getAnnotations()) { String anName = ann.annotationType().getName(); if (anName.equals("dev.langchain4j.agent.tool.Tool") - || anName.equals("ai.agentspan.annotations.Tool")) { + || anName.equals("org.conductoross.conductor.ai.annotations.Tool")) { try { - String name = (String) ann.annotationType().getMethod("name").invoke(ann); + String name = + (String) ann.annotationType().getMethod("name").invoke(ann); if (name != null && !name.isEmpty()) return name; - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } } return method.getName(); @@ -200,13 +216,15 @@ private static String resolveDescription(Method method) { String[] parts = (String[]) value; if (parts.length > 0) return String.join(" ", parts); } - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } - if (anName.equals("ai.agentspan.annotations.Tool")) { + if (anName.equals("org.conductoross.conductor.ai.annotations.Tool")) { try { Object value = ann.annotationType().getMethod("value").invoke(ann); if (value instanceof String) return (String) value; - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } } return ""; @@ -229,8 +247,7 @@ private static Map buildInputSchema(Method method) { return schema; } - private static final java.util.Set WARNED_ARG_METHODS = - java.util.concurrent.ConcurrentHashMap.newKeySet(); + private static final java.util.Set WARNED_ARG_METHODS = java.util.concurrent.ConcurrentHashMap.newKeySet(); private static String resolveParamName(Parameter p, int idx) { for (java.lang.annotation.Annotation ann : p.getAnnotations()) { @@ -238,7 +255,8 @@ private static String resolveParamName(Parameter p, int idx) { try { String v = (String) ann.annotationType().getMethod("value").invoke(ann); if (v != null && !v.isEmpty()) return v; - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } } String name = p.getName(); @@ -246,14 +264,16 @@ private static String resolveParamName(Parameter p, int idx) { // Compiler-retained names require javac -parameters. Without it the // LLM sees meaningless arg0/arg1 — warn once-per-method so the user // notices instead of silently shipping a garbage schema. - String key = p.getDeclaringExecutable().getDeclaringClass().getName() - + "#" + p.getDeclaringExecutable().getName(); + String key = p.getDeclaringExecutable().getDeclaringClass().getName() + "#" + + p.getDeclaringExecutable().getName(); if (WARNED_ARG_METHODS.add(key)) { - org.slf4j.LoggerFactory.getLogger(OpenAIAgent.class).warn( - "OpenAIAgent: method '{}' parameter names are not preserved. " - + "The LLM will see meaningless 'arg{}' parameter names. Compile " - + "with javac -parameters or use @P(\"...\") on each parameter.", - key, idx); + org.slf4j.LoggerFactory.getLogger(OpenAIAgent.class) + .warn( + "OpenAIAgent: method '{}' parameter names are not preserved. " + + "The LLM will see meaningless 'arg{}' parameter names. Compile " + + "with javac -parameters or use @P(\"...\") on each parameter.", + key, + idx); } return "arg" + idx; } @@ -269,10 +289,9 @@ private static Object[] buildMethodArgs(Method method, Map input // via Jackson. Without this, declaring a LocalDate / List // / enum param on an @Tool method would throw IllegalArgument at // invoke time. - args[i] = ai.agentspan.internal.ToolRegistry.coerceArgument( + args[i] = org.conductoross.conductor.ai.internal.ToolRegistry.coerceArgument( raw, params[i].getType(), params[i].getParameterizedType()); } return args; } - } diff --git a/sdk/java/src/main/java/ai/agentspan/gate/TextGate.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/gate/TextGate.java similarity index 96% rename from sdk/java/src/main/java/ai/agentspan/gate/TextGate.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/gate/TextGate.java index a428b72e2..f4a6ac7de 100644 --- a/sdk/java/src/main/java/ai/agentspan/gate/TextGate.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/gate/TextGate.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.gate; +package org.conductoross.conductor.ai.gate; /** * Stop a sequential pipeline if the agent's output contains the given text. diff --git a/sdk/java/src/main/java/ai/agentspan/guardrail/Guardrail.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/guardrail/Guardrail.java similarity index 81% rename from sdk/java/src/main/java/ai/agentspan/guardrail/Guardrail.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/guardrail/Guardrail.java index cf39b2d82..1bb9d5d52 100644 --- a/sdk/java/src/main/java/ai/agentspan/guardrail/Guardrail.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/guardrail/Guardrail.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.guardrail; - -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.GuardrailResult; +package org.conductoross.conductor.ai.guardrail; import java.util.function.Function; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.GuardrailResult; + /** * A custom or external validation guardrail for agent input or output. * @@ -65,9 +65,20 @@ private Builder(String name, Function func, boolean isE this.isExternal = isExternal; } - public Builder position(Position position) { this.position = position; return this; } - public Builder onFail(OnFail onFail) { this.onFail = onFail; return this; } - public Builder maxRetries(int maxRetries) { this.maxRetries = maxRetries; return this; } + public Builder position(Position position) { + this.position = position; + return this; + } + + public Builder onFail(OnFail onFail) { + this.onFail = onFail; + return this; + } + + public Builder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } public GuardrailDef build() { String guardrailType = isExternal ? "external" : "custom"; diff --git a/sdk/java/src/main/java/ai/agentspan/guardrail/LLMGuardrail.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/guardrail/LLMGuardrail.java similarity index 68% rename from sdk/java/src/main/java/ai/agentspan/guardrail/LLMGuardrail.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/guardrail/LLMGuardrail.java index 47a934633..81436ccf4 100644 --- a/sdk/java/src/main/java/ai/agentspan/guardrail/LLMGuardrail.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/guardrail/LLMGuardrail.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.guardrail; - -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.model.GuardrailDef; +package org.conductoross.conductor.ai.guardrail; import java.util.LinkedHashMap; import java.util.Map; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.model.GuardrailDef; + /** * A guardrail that uses an LLM to evaluate content against a policy. * @@ -42,21 +42,42 @@ public static class Builder { private int maxRetries = 3; private Integer maxTokens; - public Builder name(String name) { this.name = name; return this; } + public Builder name(String name) { + this.name = name; + return this; + } /** LLM model in {@code "provider/model"} format (e.g. {@code "openai/gpt-4o-mini"}). */ - public Builder model(String model) { this.model = model; return this; } + public Builder model(String model) { + this.model = model; + return this; + } /** Description of what the guardrail should check for. */ - public Builder policy(String policy) { this.policy = policy; return this; } + public Builder policy(String policy) { + this.policy = policy; + return this; + } - public Builder position(Position position) { this.position = position; return this; } + public Builder position(Position position) { + this.position = position; + return this; + } - public Builder onFail(OnFail onFail) { this.onFail = onFail; return this; } + public Builder onFail(OnFail onFail) { + this.onFail = onFail; + return this; + } - public Builder maxRetries(int maxRetries) { this.maxRetries = maxRetries; return this; } + public Builder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } - public Builder maxTokens(int maxTokens) { this.maxTokens = maxTokens; return this; } + public Builder maxTokens(int maxTokens) { + this.maxTokens = maxTokens; + return this; + } public GuardrailDef build() { if (model == null || model.isEmpty()) { diff --git a/sdk/java/src/main/java/ai/agentspan/guardrail/RegexGuardrail.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/guardrail/RegexGuardrail.java similarity index 71% rename from sdk/java/src/main/java/ai/agentspan/guardrail/RegexGuardrail.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/guardrail/RegexGuardrail.java index 0dffc252f..ad9f2f2d8 100644 --- a/sdk/java/src/main/java/ai/agentspan/guardrail/RegexGuardrail.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/guardrail/RegexGuardrail.java @@ -1,17 +1,17 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.guardrail; - -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.model.GuardrailDef; +package org.conductoross.conductor.ai.guardrail; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.model.GuardrailDef; + /** * A guardrail that validates content against regex patterns. * @@ -55,9 +55,15 @@ public static class Builder { private int maxRetries = 3; private String message; - public Builder name(String name) { this.name = name; return this; } + public Builder name(String name) { + this.name = name; + return this; + } - public Builder patterns(List patterns) { this.patterns = patterns; return this; } + public Builder patterns(List patterns) { + this.patterns = patterns; + return this; + } public Builder patterns(String... patterns) { this.patterns = Arrays.asList(patterns); @@ -65,15 +71,30 @@ public Builder patterns(String... patterns) { } /** {@code "block"} (default) or {@code "allow"}. */ - public Builder mode(String mode) { this.mode = mode; return this; } + public Builder mode(String mode) { + this.mode = mode; + return this; + } - public Builder position(Position position) { this.position = position; return this; } + public Builder position(Position position) { + this.position = position; + return this; + } - public Builder onFail(OnFail onFail) { this.onFail = onFail; return this; } + public Builder onFail(OnFail onFail) { + this.onFail = onFail; + return this; + } - public Builder maxRetries(int maxRetries) { this.maxRetries = maxRetries; return this; } + public Builder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } - public Builder message(String message) { this.message = message; return this; } + public Builder message(String message) { + this.message = message; + return this; + } public GuardrailDef build() { if (patterns == null || patterns.isEmpty()) { diff --git a/sdk/java/src/main/java/ai/agentspan/handoff/Handoff.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/handoff/Handoff.java similarity index 92% rename from sdk/java/src/main/java/ai/agentspan/handoff/Handoff.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/handoff/Handoff.java index 09c7f5259..75df6ef14 100644 --- a/sdk/java/src/main/java/ai/agentspan/handoff/Handoff.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/handoff/Handoff.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.handoff; +package org.conductoross.conductor.ai.handoff; /** * Base class for condition-based handoff triggers. diff --git a/sdk/java/src/main/java/ai/agentspan/handoff/OnCondition.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/handoff/OnCondition.java similarity index 95% rename from sdk/java/src/main/java/ai/agentspan/handoff/OnCondition.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/handoff/OnCondition.java index 58cc0d280..a2b225310 100644 --- a/sdk/java/src/main/java/ai/agentspan/handoff/OnCondition.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/handoff/OnCondition.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.handoff; +package org.conductoross.conductor.ai.handoff; import java.util.Map; import java.util.function.Function; diff --git a/sdk/java/src/main/java/ai/agentspan/handoff/OnTextMention.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/handoff/OnTextMention.java similarity index 93% rename from sdk/java/src/main/java/ai/agentspan/handoff/OnTextMention.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/handoff/OnTextMention.java index 8b5684f4d..540b329cd 100644 --- a/sdk/java/src/main/java/ai/agentspan/handoff/OnTextMention.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/handoff/OnTextMention.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.handoff; +package org.conductoross.conductor.ai.handoff; /** * Triggers a handoff when the agent output contains a specific text. diff --git a/sdk/java/src/main/java/ai/agentspan/handoff/OnToolResult.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/handoff/OnToolResult.java similarity index 84% rename from sdk/java/src/main/java/ai/agentspan/handoff/OnToolResult.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/handoff/OnToolResult.java index 9f137b97c..2dfc4374d 100644 --- a/sdk/java/src/main/java/ai/agentspan/handoff/OnToolResult.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/handoff/OnToolResult.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.handoff; +package org.conductoross.conductor.ai.handoff; /** * Triggers a handoff when a specific tool returns a result (optionally containing text). @@ -30,6 +30,11 @@ public static OnToolResult of(String toolName, String target, String resultConta return new OnToolResult(toolName, target, resultContains); } - public String getToolName() { return toolName; } - public String getResultContains() { return resultContains; } + public String getToolName() { + return toolName; + } + + public String getResultContains() { + return resultContains; + } } diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentClient.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentClient.java new file mode 100644 index 000000000..75763eddc --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentClient.java @@ -0,0 +1,115 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import org.conductoross.conductor.ai.exceptions.AgentAPIException; +import org.conductoross.conductor.ai.exceptions.AgentNotFoundException; +import org.conductoross.conductor.ai.model.CompileResponse; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.netflix.conductor.client.exception.ConductorClientException; +import com.netflix.conductor.client.http.ConductorClient; +import com.netflix.conductor.client.http.ConductorClientRequest; +import com.netflix.conductor.client.http.ConductorClientRequest.Method; +import com.netflix.conductor.client.http.ConductorClientResponse; + +/** + * Client for agentspan's proprietary agent control-plane ({@code /api/agent/*}). + * + *

    Strictly scoped to five endpoints — compile, deploy, start, status, respond. + * Standard Conductor endpoints ({@code /api/workflow/*}, {@code /api/tasks}, etc.) + * are handled by the Conductor SDK's own typed clients ({@code WorkflowClient}, + * {@code TaskClient}, {@code MetadataClient}). + * + *

    Every request goes through the shared {@link ConductorClient}'s native HTTP + + * auth + serialization layer ({@link ConductorClientRequest} → + * {@link ConductorClient#execute}). No hand-rolled HTTP. Conductor's + * {@link ConductorClientException} is mapped to agentspan's + * {@link AgentAPIException}/{@link AgentNotFoundException}. + * + *

    Paths are relative to the client's base path (the server's {@code /api} + * root), so {@code "/agent/start"} resolves to {@code /api/agent/start}. + */ +public class AgentClient { + + private static final TypeReference COMPILE_TYPE = new TypeReference() {}; + private static final TypeReference START_TYPE = new TypeReference() {}; + private static final TypeReference STATUS_TYPE = new TypeReference() {}; + + protected final ConductorClient client; + + public AgentClient(ConductorClient client) { + this.client = client; + } + + /** {@code POST /api/agent/compile} — compile agent config to a workflow def. */ + public CompileResponse compileAgent(AgentRequest request) { + return post("/agent/compile", request, COMPILE_TYPE); + } + + /** {@code POST /api/agent/deploy} — compile + register, no execution. */ + public StartResponse deployAgent(AgentRequest request) { + return post("/agent/deploy", request, START_TYPE); + } + + /** {@code POST /api/agent/start} — compile + register + start an execution. */ + public StartResponse startAgent(AgentRequest request) { + return post("/agent/start", request, START_TYPE); + } + + /** {@code GET /api/agent/{executionId}/status} — fetch execution status. */ + public AgentStatusResponse getAgentStatus(String executionId) { + ConductorClientRequest req = ConductorClientRequest.builder() + .method(Method.GET) + .path("/agent/{executionId}/status") + .addPathParam("executionId", executionId) + .build(); + return executeFor(req, STATUS_TYPE); + } + + /** {@code POST /api/agent/{executionId}/respond} — respond to a waiting HITL task. */ + public void respond(String executionId, RespondBody body) { + ConductorClientRequest req = ConductorClientRequest.builder() + .method(Method.POST) + .path("/agent/{executionId}/respond") + .addPathParam("executionId", executionId) + .body(body) + .build(); + try { + client.execute(req); + } catch (ConductorClientException e) { + throw mapException(e); + } + } + + // ── internals ────────────────────────────────────────────────────────── + + private T post(String path, Object payload, TypeReference type) { + ConductorClientRequest req = ConductorClientRequest.builder() + .method(Method.POST) + .path(path) + .body(payload) + .build(); + return executeFor(req, type); + } + + private T executeFor(ConductorClientRequest req, TypeReference type) { + try { + ConductorClientResponse resp = client.execute(req, type); + return resp.getData(); + } catch (ConductorClientException e) { + throw mapException(e); + } + } + + /** Preserve agentspan's typed error contract over Conductor's exception. */ + private static RuntimeException mapException(ConductorClientException e) { + int status = e.getStatus(); + String body = e.getMessage(); + if (status == 404) { + return new AgentNotFoundException(status, body); + } + return new AgentAPIException(status, body); + } +} diff --git a/sdk/java/src/main/java/ai/agentspan/internal/AgentConfigSerializer.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentConfigSerializer.java similarity index 73% rename from sdk/java/src/main/java/ai/agentspan/internal/AgentConfigSerializer.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentConfigSerializer.java index efe5a9741..19b9e479f 100644 --- a/sdk/java/src/main/java/ai/agentspan/internal/AgentConfigSerializer.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentConfigSerializer.java @@ -1,20 +1,34 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.internal; - -import ai.agentspan.Agent; -import ai.agentspan.execution.CliConfig; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.PromptTemplate; -import ai.agentspan.model.ToolDef; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +package org.conductoross.conductor.ai.internal; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.execution.CliConfig; +import org.conductoross.conductor.ai.gate.TextGate; +import org.conductoross.conductor.ai.handoff.Handoff; +import org.conductoross.conductor.ai.handoff.OnTextMention; +import org.conductoross.conductor.ai.handoff.OnToolResult; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.PromptTemplate; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.plans.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; /** * Serializes an {@link Agent} tree to the camelCase JSON dict for POST /agent/start. @@ -58,9 +72,11 @@ private Map serializeAgent(Agent agent) { map.put("model", agent.getModel()); } // OpenAI uses `instructions`; ADK uses `instruction` (singular). - if (agent.getInstructions() != null && !agent.getInstructions().isEmpty()) { - map.put("google_adk".equals(fw) ? "instruction" : "instructions", - agent.getInstructions()); + // Resolve once: dynamic instructions are supplier-backed and must not + // be re-evaluated within a single serialization. + String fwInstructions = agent.getInstructions(); + if (fwInstructions != null && !fwInstructions.isEmpty()) { + map.put("google_adk".equals(fw) ? "instruction" : "instructions", fwInstructions); } // Tools: framework normalizers (OpenAINormalizer, GoogleADKNormalizer) // expect the worker_ref shape `{_worker_ref, description, parameters}` @@ -157,8 +173,13 @@ private Map serializeAgent(Agent agent) { tmpl.put("version", pt.getVersion()); } agentMap.put("instructions", tmpl); - } else if (agent.getInstructions() != null && !agent.getInstructions().isEmpty()) { - agentMap.put("instructions", agent.getInstructions()); + } else { + // Resolve once: dynamic instructions are supplier-backed and must not + // be re-evaluated within a single serialization. + String instructions = agent.getInstructions(); + if (instructions != null && !instructions.isEmpty()) { + agentMap.put("instructions", instructions); + } } // Tools @@ -199,11 +220,39 @@ private Map serializeAgent(Agent agent) { agentMap.put("maxTokens", agent.getMaxTokens()); } + // Context window budget for proactive condensation + if (agent.getContextWindowBudget() != null) { + agentMap.put("contextWindowBudget", agent.getContextWindowBudget()); + } + // Temperature if (agent.getTemperature() != null) { agentMap.put("temperature", agent.getTemperature()); } + // Reasoning effort (OpenAI reasoning models) + if (agent.getReasoningEffort() != null && !agent.getReasoningEffort().isEmpty()) { + agentMap.put("reasoningEffort", agent.getReasoningEffort()); + } + + // Masked fields (redacted in execution history / UI) + if (agent.getMaskedFields() != null && !agent.getMaskedFields().isEmpty()) { + agentMap.put("maskedFields", agent.getMaskedFields()); + } + + // Conversation memory + if (agent.getMemory() != null) { + Map memMap = new LinkedHashMap<>(); + if (agent.getMemory().getMessages() != null + && !agent.getMemory().getMessages().isEmpty()) { + memMap.put("messages", agent.getMemory().getMessages()); + } + if (agent.getMemory().getMaxMessages() != null) { + memMap.put("maxMessages", agent.getMemory().getMaxMessages()); + } + agentMap.put("memory", memMap); + } + // Termination condition if (agent.getTermination() != null) { agentMap.put("termination", agent.getTermination().toMap()); @@ -221,13 +270,16 @@ private Map serializeAgent(Agent agent) { // Condition-based handoffs if (agent.getHandoffs() != null && !agent.getHandoffs().isEmpty()) { - agentMap.put("handoffs", agent.getHandoffs().stream() - .map(h -> serializeHandoff(h, agent.getName())) - .collect(java.util.stream.Collectors.toList())); + agentMap.put( + "handoffs", + agent.getHandoffs().stream() + .map(h -> serializeHandoff(h, agent.getName())) + .collect(Collectors.toList())); } // Allowed transitions (constrained handoff paths) - if (agent.getAllowedTransitions() != null && !agent.getAllowedTransitions().isEmpty()) { + if (agent.getAllowedTransitions() != null + && !agent.getAllowedTransitions().isEmpty()) { agentMap.put("allowedTransitions", agent.getAllowedTransitions()); } @@ -276,18 +328,19 @@ private Map serializeAgent(Agent agent) { String execToolName = agent.getName() + "_execute_code"; Map execTool = new LinkedHashMap<>(); execTool.put("name", execToolName); - execTool.put("description", - "Execute code in the specified language. Supported languages: " - + String.join(", ", effectiveLangs) - + ". Each execution runs in an isolated environment — no state, variables, " - + "or imports persist between calls."); + execTool.put( + "description", + "Execute code in the specified language. Supported languages: " + + String.join(", ", effectiveLangs) + + ". Each execution runs in an isolated environment — no state, variables, " + + "or imports persist between calls."); Map inputSchema = new LinkedHashMap<>(); inputSchema.put("type", "object"); Map properties = new LinkedHashMap<>(); Map langProp = new LinkedHashMap<>(); langProp.put("type", "string"); - langProp.put("description", "The programming language to use. One of: " - + String.join(", ", effectiveLangs)); + langProp.put( + "description", "The programming language to use. One of: " + String.join(", ", effectiveLangs)); langProp.put("enum", effectiveLangs); Map codeProp = new LinkedHashMap<>(); codeProp.put("type", "string"); @@ -297,8 +350,7 @@ private Map serializeAgent(Agent agent) { inputSchema.put("properties", properties); inputSchema.put("required", List.of("language", "code")); execTool.put("inputSchema", inputSchema); - execTool.put("outputSchema", - Map.of("type", "object", "additionalProperties", Map.of())); + execTool.put("outputSchema", Map.of("type", "object", "additionalProperties", Map.of())); execTool.put("toolType", "worker"); // Append or create tools list @@ -323,6 +375,58 @@ private Map serializeAgent(Agent agent) { cliMap.put("allowShell", cliConfig.isAllowShell()); if (cliConfig.getWorkingDir() != null) cliMap.put("workingDir", cliConfig.getWorkingDir()); agentMap.put("cliConfig", cliMap); + + // Inject the run_command worker tool so the LLM sees it as a callable + // function and the server creates a SIMPLE task that this SDK's worker + // executes locally. Mirrors Python's Agent._attach_cli_tool(). The tool + // name is {agent_name}_run_command to avoid multi-agent collisions. + if (cliConfig.isEnabled()) { + List allowed = cliConfig.getAllowedCommands(); + + StringBuilder desc = new StringBuilder("Run a CLI command directly. Timeout: ") + .append(cliConfig.getTimeout() > 0 ? cliConfig.getTimeout() : 30) + .append("s."); + if (allowed != null && !allowed.isEmpty()) { + List sorted = new ArrayList<>(allowed); + java.util.Collections.sort(sorted); + desc.append(" Allowed commands: ") + .append(String.join(", ", sorted)) + .append('.'); + } + if (!cliConfig.isAllowShell()) { + desc.append(" Shell mode is disabled — do not set shell=true."); + } + + Map cliProps = new LinkedHashMap<>(); + cliProps.put("command", Map.of("type", "string", "description", "The CLI command to execute")); + cliProps.put( + "args", + Map.of("type", "array", "items", Map.of("type", "string"), "description", "Command arguments")); + cliProps.put("cwd", Map.of("type", "string", "description", "Working directory for the command")); + cliProps.put("shell", Map.of("type", "boolean", "description", "Whether to run via shell")); + + Map cliInputSchema = new LinkedHashMap<>(); + cliInputSchema.put("type", "object"); + cliInputSchema.put("properties", cliProps); + cliInputSchema.put("required", List.of("command")); + + Map cliTool = new LinkedHashMap<>(); + cliTool.put("name", agent.getName() + "_run_command"); + cliTool.put("description", desc.toString()); + cliTool.put("inputSchema", cliInputSchema); + cliTool.put("outputSchema", Map.of("type", "object", "additionalProperties", Map.of())); + cliTool.put("toolType", "worker"); + + @SuppressWarnings("unchecked") + List> existingTools = (List>) agentMap.get("tools"); + if (existingTools == null) { + List> toolsList = new ArrayList<>(); + toolsList.add(cliTool); + agentMap.put("tools", toolsList); + } else { + existingTools.add(cliTool); + } + } } // Include contents (context passed to sub-agent) @@ -387,7 +491,7 @@ private Map serializeAgent(Agent agent) { // via toJson() — defaults are omitted so the payload stays tight. if (agent.getPlannerContext() != null && !agent.getPlannerContext().isEmpty()) { java.util.List> ctx = new java.util.ArrayList<>(); - for (ai.agentspan.plans.Context entry : agent.getPlannerContext()) { + for (Context entry : agent.getPlannerContext()) { ctx.add(entry.toJson()); } agentMap.put("plannerContext", ctx); @@ -405,7 +509,7 @@ private Map serializeAgent(Agent agent) { // Gate (stop sequential pipeline when output contains sentinel text) if (agent.getGate() != null) { - ai.agentspan.gate.TextGate g = agent.getGate(); + TextGate g = agent.getGate(); Map gateMap = new LinkedHashMap<>(); gateMap.put("type", "text_contains"); gateMap.put("text", g.getText()); @@ -443,30 +547,30 @@ private Map serializeAgent(Agent agent) { if (agent.getCallbacks() != null && !agent.getCallbacks().isEmpty()) { String[][] positionMethods = { {"before_agent", "onAgentStart"}, - {"after_agent", "onAgentEnd"}, + {"after_agent", "onAgentEnd"}, {"before_model", "onModelStart"}, - {"after_model", "onModelEnd"}, - {"before_tool", "onToolStart"}, - {"after_tool", "onToolEnd"}, + {"after_model", "onModelEnd"}, + {"before_tool", "onToolStart"}, + {"after_tool", "onToolEnd"}, }; for (String[] pm : positionMethods) { String position = pm[0]; String methodName = pm[1]; // Check if any handler overrides this method boolean hasOverride = false; - for (ai.agentspan.CallbackHandler h : agent.getCallbacks()) { + for (org.conductoross.conductor.ai.CallbackHandler h : agent.getCallbacks()) { try { - java.lang.reflect.Method m = h.getClass().getMethod(methodName, Map.class); - if (!m.getDeclaringClass().equals(ai.agentspan.CallbackHandler.class)) { + Method m = h.getClass().getMethod(methodName, Map.class); + if (!m.getDeclaringClass().equals(org.conductoross.conductor.ai.CallbackHandler.class)) { hasOverride = true; break; } - } catch (NoSuchMethodException ignored) {} + } catch (NoSuchMethodException ignored) { + } } if (hasOverride) { // Only add if not already present from legacy callbacks - boolean alreadyAdded = callbacks.stream() - .anyMatch(c -> position.equals(c.get("position"))); + boolean alreadyAdded = callbacks.stream().anyMatch(c -> position.equals(c.get("position"))); if (!alreadyAdded) { Map cb = new LinkedHashMap<>(); cb.put("position", position); @@ -493,8 +597,9 @@ private Map serializeTool(ToolDef tool, boolean agentStateful) { } if ("worker".equals(tool.getToolType())) { Map outSchema = tool.getOutputSchema(); - toolMap.put("outputSchema", outSchema != null ? outSchema - : Map.of("type", "object", "additionalProperties", Map.of())); + toolMap.put( + "outputSchema", + outSchema != null ? outSchema : Map.of("type", "object", "additionalProperties", Map.of())); } toolMap.put("toolType", tool.getToolType()); @@ -531,23 +636,25 @@ private Map serializeTool(ToolDef tool, boolean agentStateful) { } if (tool.getGuardrails() != null && !tool.getGuardrails().isEmpty()) { - toolMap.put("guardrails", tool.getGuardrails().stream() - .map(g -> serializeGuardrail(g, tool.getName())) - .collect(java.util.stream.Collectors.toList())); + toolMap.put( + "guardrails", + tool.getGuardrails().stream() + .map(g -> serializeGuardrail(g, tool.getName())) + .collect(Collectors.toList())); } return toolMap; } - private Map serializeHandoff(ai.agentspan.handoff.Handoff h, String agentName) { + private Map serializeHandoff(Handoff h, String agentName) { Map hMap = new LinkedHashMap<>(); hMap.put("target", h.getTarget()); - if (h instanceof ai.agentspan.handoff.OnTextMention) { - ai.agentspan.handoff.OnTextMention otm = (ai.agentspan.handoff.OnTextMention) h; + if (h instanceof OnTextMention) { + OnTextMention otm = (OnTextMention) h; hMap.put("type", "on_text_mention"); hMap.put("text", otm.getText()); - } else if (h instanceof ai.agentspan.handoff.OnToolResult) { - ai.agentspan.handoff.OnToolResult otr = (ai.agentspan.handoff.OnToolResult) h; + } else if (h instanceof OnToolResult) { + OnToolResult otr = (OnToolResult) h; hMap.put("type", "on_tool_result"); hMap.put("toolName", otr.getToolName()); if (otr.getResultContains() != null) { @@ -596,8 +703,8 @@ private Map generateJsonSchema(Class cls) { Map properties = new LinkedHashMap<>(); List required = new ArrayList<>(); - for (java.lang.reflect.Field field : cls.getDeclaredFields()) { - if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) continue; + for (Field field : cls.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) continue; Map propSchema = ToolRegistry.typeToJsonSchema(field.getType()); properties.put(field.getName(), propSchema); required.add(field.getName()); @@ -609,4 +716,20 @@ private Map generateJsonSchema(Class cls) { } return schema; } + + /** + * Jackson {@link JsonSerializer} that delegates to {@link AgentConfigSerializer#serialize(Agent)}. + * Applied via {@code @JsonSerialize(using = AgentConfigSerializer.AsJson.class)} on + * {@code Agent}-typed fields in {@link AgentRequest} so Jackson writes the correct + * wire format (camelCase map matching the server's AgentConfig DTO) without + * requiring the caller to pre-serialize to a Map. + */ + public static final class AsJson extends JsonSerializer { + private static final AgentConfigSerializer INSTANCE = new AgentConfigSerializer(); + + @Override + public void serialize(Agent agent, JsonGenerator gen, SerializerProvider provider) throws IOException { + provider.defaultSerializeValue(INSTANCE.serialize(agent), gen); + } + } } diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentRegistry.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentRegistry.java new file mode 100644 index 000000000..a0b93d194 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentRegistry.java @@ -0,0 +1,350 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.annotations.AgentDef; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.PromptTemplate; +import org.conductoross.conductor.ai.model.ToolDef; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Discovers {@link AgentDef}-annotated methods + * via reflection and resolves them into {@link Agent} instances. + * + *

    Parallel to {@link ToolRegistry}. Prefer the public entry points + * {@link Agent#fromInstance(Object)} and {@link Agent#fromInstance(Object, String)}. + */ +public final class AgentRegistry { + private static final Logger logger = LoggerFactory.getLogger(AgentRegistry.class); + + private AgentRegistry() {} + + /** + * Resolve all {@code @AgentDef}-annotated methods on an object into Agent instances. + * + * @param obj the object to inspect + * @return list of resolved agents + */ + public static List fromInstance(Object obj) { + Map methods = agentMethods(obj); + List agents = new ArrayList<>(); + for (Map.Entry entry : methods.entrySet()) { + agents.add(resolve(obj, methods, entry.getKey(), "", new ArrayDeque<>())); + } + return agents; + } + + /** + * Resolve a single {@code @AgentDef}-annotated method by its resolved agent name. + * + * @param obj the object to inspect + * @param name the agent name (annotation {@code name} or the method name) + * @return the resolved agent + * @throws IllegalArgumentException if no agent with that name is defined on the object + */ + public static Agent fromInstance(Object obj, String name) { + Map methods = agentMethods(obj); + if (!methods.containsKey(name)) { + throw new IllegalArgumentException("No @AgentDef method named '" + name + "' on " + + obj.getClass().getName() + ". Available: " + methods.keySet()); + } + return resolve(obj, methods, name, "", new ArrayDeque<>()); + } + + /** + * Discover all {@code @AgentDef}-annotated methods, keyed by resolved agent name. + * + *

    Walks the full type hierarchy (superclasses, then interfaces) rather than + * {@code getMethods()}, for two reasons: + *

      + *
    • An unannotated override must not hide an annotated ancestor declaration — + * a CGLIB-style proxy (e.g. a {@code @Transactional} Spring bean) overrides + * every public method without copying annotations. The ancestor's annotated + * {@link Method} is used; invocation still dispatches virtually, so the + * override (proxy behavior) executes. Nearest annotated declaration wins.
    • + *
    • A non-public annotated method is a user error and must fail loudly, not + * be silently invisible.
    • + *
    + */ + private static Map agentMethods(Object obj) { + Map methods = new LinkedHashMap<>(); + Set claimedSignatures = new HashSet<>(); + Deque> queue = new ArrayDeque<>(); + Set> visited = new HashSet<>(); + for (Class c = obj.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { + queue.add(c); + } + while (!queue.isEmpty()) { + Class type = queue.poll(); + if (!visited.add(type)) continue; + queue.addAll(Arrays.asList(type.getInterfaces())); + for (Method method : type.getDeclaredMethods()) { + if (method.isSynthetic()) continue; + AgentDef ann = method.getAnnotation(AgentDef.class); + if (ann == null) continue; + String signature = method.getName() + Arrays.toString(method.getParameterTypes()); + if (!claimedSignatures.add(signature)) continue; // nearer declaration already claimed it + if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException( + "@AgentDef method " + method.getName() + " on " + type.getName() + " must be public"); + } + if (method.isAnnotationPresent(Tool.class) + || method.isAnnotationPresent(org.conductoross.conductor.ai.annotations.GuardrailDef.class)) { + throw new IllegalArgumentException("Method " + method.getName() + " on " + type.getName() + + " cannot combine @AgentDef with @Tool or @GuardrailDef"); + } + String name = ann.name().isEmpty() ? method.getName() : ann.name(); + Method previous = methods.put(name, method); + if (previous != null) { + throw new IllegalArgumentException("Duplicate @AgentDef name '" + name + "' on " + + obj.getClass().getName() + " (methods " + previous.getName() + " and " + + method.getName() + ")"); + } + } + } + return methods; + } + + private static Agent resolve( + Object obj, Map methods, String name, String parentModel, Deque stack) { + if (stack.contains(name)) { + List cycle = new ArrayList<>(stack); + cycle.add(name); + throw new IllegalArgumentException("Cyclic @AgentDef sub-agent reference: " + String.join(" -> ", cycle)); + } + stack.push(name); + try { + Method method = methods.get(name); + AgentDef ann = method.getAnnotation(AgentDef.class); + validateSignature(obj, method); + + // Pure factory: a no-arg method returning Agent or Agent.Builder builds the + // whole definition itself (CrewAI-style). The annotation is a discovery + // marker only — non-default attributes would be silently ignored, so reject. + Class returnType = method.getReturnType(); + boolean pureFactory = + method.getParameterCount() == 0 && (returnType == Agent.class || returnType == Agent.Builder.class); + if (pureFactory) { + requireDiscoveryOnlyAttributes(obj, method, ann); + return buildFromFactoryResult(obj, method, invoke(obj, method)); + } + + String model = !ann.model().isEmpty() ? ann.model() : parentModel; + + Agent.Builder builder = Agent.builder() + .name(name) + .instructions(ann.instructions()) + .maxTurns(ann.maxTurns()) + .strategy(ann.strategy()); + if (!model.isEmpty()) builder.model(model); + if (ann.maxTokens() > 0) builder.maxTokens(ann.maxTokens()); + if (!Double.isNaN(ann.temperature())) builder.temperature(ann.temperature()); + if (ann.credentials().length > 0) builder.credentials(Arrays.asList(ann.credentials())); + if (ann.contextWindowBudget() > 0) builder.contextWindowBudget(ann.contextWindowBudget()); + + List tools = + selectByName(ToolRegistry.fromInstance(obj), ToolDef::getName, ann.tools(), "@Tool", obj); + if (!tools.isEmpty()) builder.tools(tools); + + List guardrails = selectByName( + ToolRegistry.guardrailsFromInstance(obj), + GuardrailDef::getName, + ann.guardrails(), + "@GuardrailDef", + obj); + if (!guardrails.isEmpty()) builder.guardrails(guardrails); + + if (ann.agents().length > 0) { + List subAgents = new ArrayList<>(); + for (String subName : ann.agents()) { + if (!methods.containsKey(subName)) { + throw new IllegalArgumentException("Sub-agent '" + subName + "' referenced by @AgentDef '" + + name + "' not found on " + obj.getClass().getName() + + ". Available: " + methods.keySet()); + } + subAgents.add(resolve(obj, methods, subName, model, stack)); + } + builder.agents(subAgents); + } + + Agent agent = invokeAgentMethod(obj, method, ann, builder); + logger.debug("Resolved agent '{}' from {}", name, obj.getClass().getSimpleName()); + return agent; + } finally { + stack.pop(); + } + } + + /** + * Enforce the {@code @AgentDef} method contract. The return type declares what + * the method provides: + *
      + *
    • {@code void} — nothing; the annotation alone defines the agent
    • + *
    • {@code String} — dynamic instructions
    • + *
    • {@code PromptTemplate} — a server-side instructions template
    • + *
    • {@code Agent.Builder} — the definition itself; the returned builder is built
    • + *
    • {@code Agent} — the definition itself, returned as-is (full factory)
    • + *
    + * Parameters: none, or a single {@link Agent.Builder} (pre-populated from the + * annotation and discovered tools/guardrails/sub-agents). + */ + private static void validateSignature(Object obj, Method method) { + Class returnType = method.getReturnType(); + if (returnType != String.class + && returnType != void.class + && returnType != Void.class + && returnType != PromptTemplate.class + && returnType != Agent.class + && returnType != Agent.Builder.class) { + throw new IllegalArgumentException("@AgentDef method " + method.getName() + " on " + + obj.getClass().getName() + + " must return String, PromptTemplate, Agent, Agent.Builder, or void; got " + + returnType.getSimpleName()); + } + Class[] params = method.getParameterTypes(); + if (params.length > 1 || (params.length == 1 && params[0] != Agent.Builder.class)) { + throw new IllegalArgumentException("@AgentDef method " + method.getName() + " on " + + obj.getClass().getName() + + " must take no parameters, or a single Agent.Builder to customize"); + } + } + + /** + * Invoke the agent method after the builder is pre-populated from the + * annotation, then build the agent. Dispatch is by declared return type: + * + *
      + *
    • {@code void}, no-arg — pure marker; never invoked.
    • + *
    • {@code void} + builder param — customizer; invoked once.
    • + *
    • {@code String}, no-arg — lazy dynamic instructions: the method is + * re-invoked every time {@link Agent#getInstructions()} resolves (each run + * submission), matching the Python SDK where callable instructions resolve + * at serialization time. A non-empty result wins over the annotation + * attribute.
    • + *
    • {@code String} + builder param — invoked once, eagerly: re-running a + * customizer per serialization would replay its side effects.
    • + *
    • {@code PromptTemplate} — invoked once; a non-null result becomes + * {@code instructionsTemplate}.
    • + *
    • {@code Agent.Builder} / {@code Agent} + builder param — the returned + * value is the definition (built if a builder).
    • + *
    + */ + private static Agent invokeAgentMethod(Object obj, Method method, AgentDef ann, Agent.Builder builder) { + Class returnType = method.getReturnType(); + boolean wantsBuilder = method.getParameterCount() == 1; + + if (returnType == String.class && !wantsBuilder) { + builder.instructions(() -> { + Object dynamic = invoke(obj, method); + return (dynamic instanceof String s && !s.isEmpty()) ? s : ann.instructions(); + }); + return builder.build(); + } + + Object result = (returnType == void.class || returnType == Void.class) && !wantsBuilder + ? null // pure marker — nothing to invoke + : (wantsBuilder ? invoke(obj, method, builder) : invoke(obj, method)); + + if (returnType == String.class) { + if (result instanceof String dynamic && !dynamic.isEmpty()) { + builder.instructions(dynamic); + } + } else if (returnType == PromptTemplate.class) { + if (result != null) { + builder.instructionsTemplate((PromptTemplate) result); + } + } else if (returnType == Agent.class || returnType == Agent.Builder.class) { + return buildFromFactoryResult(obj, method, result); + } + return builder.build(); + } + + /** Turn an {@code Agent}/{@code Agent.Builder} factory result into the agent. */ + private static Agent buildFromFactoryResult(Object obj, Method method, Object result) { + if (result == null) { + throw new IllegalArgumentException("@AgentDef factory method " + method.getName() + " on " + + obj.getClass().getName() + " returned null; it must return the agent definition"); + } + return result instanceof Agent.Builder b ? b.build() : (Agent) result; + } + + /** + * Reject non-default annotation attributes on a pure factory method (no-arg, + * returning {@code Agent} or {@code Agent.Builder}) — the factory builds the + * whole definition, so attributes other than {@code name} would be silently + * ignored. Methods that accept the pre-populated builder may use attributes. + */ + private static void requireDiscoveryOnlyAttributes(Object obj, Method method, AgentDef ann) { + List set = new ArrayList<>(); + if (!ann.model().isEmpty()) set.add("model"); + if (!ann.instructions().isEmpty()) set.add("instructions"); + if (!Arrays.equals(ann.tools(), new String[] {"*"})) set.add("tools"); + if (!Arrays.equals(ann.guardrails(), new String[] {"*"})) set.add("guardrails"); + if (ann.agents().length > 0) set.add("agents"); + if (ann.strategy() != Strategy.HANDOFF) set.add("strategy"); + if (ann.maxTurns() != 25) set.add("maxTurns"); + if (ann.maxTokens() != 0) set.add("maxTokens"); + if (!Double.isNaN(ann.temperature())) set.add("temperature"); + if (ann.credentials().length > 0) set.add("credentials"); + if (ann.contextWindowBudget() != 0) set.add("contextWindowBudget"); + if (!set.isEmpty()) { + throw new IllegalArgumentException("@AgentDef factory method " + method.getName() + " on " + + obj.getClass().getName() + " returns " + + method.getReturnType().getSimpleName() + + " and builds the definition itself, but sets annotation attributes " + set + + " that would be ignored. Either drop the attributes, or accept the" + + " pre-populated Agent.Builder as a parameter."); + } + } + + /** Reflectively invoke the agent method, unwrapping reflection exceptions. */ + private static Object invoke(Object obj, Method method, Object... args) { + try { + method.setAccessible(true); + return method.invoke(obj, args); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Failed to invoke @AgentDef method " + method.getName(), e); + } + } + + /** + * Filter discovered definitions by the annotation's name list: + * {@code {"*"}} selects all, an empty array selects none, otherwise match by + * name and throw on unknown names. + */ + private static List selectByName( + List all, java.util.function.Function nameOf, String[] requested, String kind, Object obj) { + if (requested.length == 1 && "*".equals(requested[0])) { + return all; + } + List selected = new ArrayList<>(); + for (String want : requested) { + T match = all.stream() + .filter(t -> want.equals(nameOf.apply(t))) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No " + kind + " method named '" + want + + "' on " + obj.getClass().getName() + ". Available: " + + all.stream().map(nameOf).toList())); + selected.add(match); + } + return selected; + } +} diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentRequest.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentRequest.java new file mode 100644 index 000000000..1d892a815 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentRequest.java @@ -0,0 +1,202 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.enums.Framework; +import org.conductoross.conductor.ai.plans.Plan; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +/** + * Request payload for {@code POST /api/agent/compile}, {@code /deploy}, and {@code /start}. + * + *

    All three endpoints share the same server-side {@code StartRequest} DTO. A single + * {@link Agent} field carries the agent definition. The {@link Serializer} writes it + * under either {@code "agentConfig"} (native agents) or {@code "framework"} + + * {@code "rawConfig"} (framework-backed agents) — no duplication. + * + *

    Build via {@link #nativeAgent(Agent)} or {@link #frameworkAgent(Framework, Agent)}, + * then chain builder methods for execution-specific fields. + */ +@JsonSerialize(using = AgentRequest.Serializer.class) +public final class AgentRequest { + + // ── Agent definition ──────────────────────────────────────────────── + /** The agent to compile / deploy / start. Always present. */ + final Agent agent; + + /** + * Non-null for framework-backed agents; {@code null} for native agents. + * Determines whether {@link Serializer} writes {@code "agentConfig"} or + * {@code "framework"} + {@code "rawConfig"}. + */ + final Framework framework; + + // ── Execution fields (only meaningful for /start) ──────────────────── + final String prompt; + final String sessionId; + final String runId; + final Plan staticPlan; + + // ── Optional fields ────────────────────────────────────────────────── + final List media; + final Map context; + final String idempotencyKey; + final List credentials; + final Integer timeoutSeconds; + + private AgentRequest(Builder b) { + this.agent = b.agent; + this.framework = b.framework; + this.prompt = b.prompt; + this.sessionId = b.sessionId; + this.runId = b.runId; + this.staticPlan = b.staticPlan; + this.media = b.media; + this.context = b.context; + this.idempotencyKey = b.idempotencyKey; + this.credentials = b.credentials; + this.timeoutSeconds = b.timeoutSeconds; + } + + /** Build a request for a native (non-framework) agent. */ + public static Builder nativeAgent(Agent agent) { + return new Builder(agent, null); + } + + /** Build a request for a framework-backed agent (OpenAI, ADK, Skill). */ + public static Builder frameworkAgent(Framework framework, Agent agent) { + return new Builder(agent, framework); + } + + // ── Builder ────────────────────────────────────────────────────────── + + public static final class Builder { + private final Agent agent; + private final Framework framework; + private String prompt; + private String sessionId; + private String runId; + private Plan staticPlan; + private List media; + private Map context; + private String idempotencyKey; + private List credentials; + private Integer timeoutSeconds; + + private Builder(Agent agent, Framework framework) { + this.agent = agent; + this.framework = framework; + } + + public Builder prompt(String v) { + this.prompt = v; + return this; + } + + public Builder sessionId(String v) { + this.sessionId = v; + return this; + } + + public Builder runId(String v) { + this.runId = v; + return this; + } + + public Builder staticPlan(Plan v) { + this.staticPlan = v; + return this; + } + + public Builder media(List v) { + this.media = v; + return this; + } + + public Builder context(Map v) { + this.context = v; + return this; + } + + public Builder idempotencyKey(String v) { + this.idempotencyKey = v; + return this; + } + + public Builder credentials(List v) { + this.credentials = v; + return this; + } + + public Builder timeoutSeconds(Integer v) { + this.timeoutSeconds = v; + return this; + } + + public AgentRequest build() { + return new AgentRequest(this); + } + } + + // ── Jackson serializer ─────────────────────────────────────────────── + + /** + * Writes the correct JSON shape based on whether a {@link Framework} is set: + *

      + *
    • Native: {@code "agentConfig": serialize(agent)}
    • + *
    • Framework: {@code "framework": "openai", "rawConfig": serialize(agent)}
    • + *
    + * All other fields are written with explicit null-checks so no field is emitted + * when not set ({@code @JsonInclude(NON_NULL)} is not needed on the class). + */ + static final class Serializer extends JsonSerializer { + private static final AgentConfigSerializer AGENT_SERIALIZER = new AgentConfigSerializer(); + + @Override + public void serialize(AgentRequest r, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeStartObject(); + + // Agent definition — mutually exclusive key based on framework + if (r.framework == null) { + gen.writeObjectField("agentConfig", AGENT_SERIALIZER.serialize(r.agent)); + } else { + gen.writeStringField("framework", r.framework.wireValue()); + gen.writeObjectField("rawConfig", AGENT_SERIALIZER.serialize(r.agent)); + } + + // Execution fields + if (r.prompt != null) gen.writeStringField("prompt", r.prompt); + if (r.sessionId != null) gen.writeStringField("sessionId", r.sessionId); + if (r.runId != null) gen.writeStringField("runId", r.runId); + if (r.staticPlan != null) gen.writeObjectField("static_plan", r.staticPlan.toJson()); + + // Optional fields + if (r.media != null) { + gen.writeFieldName("media"); + provider.defaultSerializeValue(r.media, gen); + } + if (r.context != null) { + gen.writeFieldName("context"); + provider.defaultSerializeValue(r.context, gen); + } + if (r.idempotencyKey != null) gen.writeStringField("idempotencyKey", r.idempotencyKey); + if (r.credentials != null) { + gen.writeFieldName("credentials"); + provider.defaultSerializeValue(r.credentials, gen); + } + if (r.timeoutSeconds != null) gen.writeNumberField("timeoutSeconds", r.timeoutSeconds); + + gen.writeEndObject(); + } + } +} diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentStatusResponse.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentStatusResponse.java new file mode 100644 index 000000000..07424378c --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/AgentStatusResponse.java @@ -0,0 +1,99 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response from {@code GET /api/agent/{executionId}/status}. + * + *

    Polled by {@link org.conductoross.conductor.ai.model.AgentHandle} until the + * execution reaches a terminal status. Used internally — callers receive an + * {@link org.conductoross.conductor.ai.model.AgentResult} after completion. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class AgentStatusResponse { + + @JsonProperty("executionId") + private String executionId; + + @JsonProperty("status") + private String status; + + @JsonProperty("isComplete") + private boolean complete; + + @JsonProperty("isRunning") + private boolean running; + + @JsonProperty("isWaiting") + private boolean waiting; + + @JsonProperty("output") + private Map output; + + @JsonProperty("reasonForIncompletion") + private String reasonForIncompletion; + + @JsonProperty("pendingTool") + private PendingTool pendingTool; + + public AgentStatusResponse() {} + + public String getExecutionId() { + return executionId; + } + + /** + * Conductor workflow status string: {@code RUNNING}, {@code COMPLETED}, + * {@code FAILED}, {@code TERMINATED}, {@code TIMED_OUT}, {@code PAUSED}. + */ + public String getStatus() { + return status; + } + + /** {@code true} when status is terminal (COMPLETED, FAILED, TERMINATED, TIMED_OUT). */ + public boolean isComplete() { + return complete; + } + + public boolean isRunning() { + return running; + } + + /** {@code true} when a HITL task is paused waiting for human input. */ + public boolean isWaiting() { + return waiting; + } + + /** + * Final workflow output. Only present when {@link #isComplete()} is {@code true}. + */ + public Map getOutput() { + return output; + } + + /** + * Failure or termination reason. Only present for non-COMPLETED terminal runs. + */ + public String getReasonForIncompletion() { + return reasonForIncompletion; + } + + /** + * Details of the paused HITL task. Only present when {@link #isWaiting()} is {@code true}. + */ + public PendingTool getPendingTool() { + return pendingTool; + } + + @Override + public String toString() { + return "AgentStatusResponse{executionId=" + executionId + ", status=" + status + ", complete=" + complete + + ", waiting=" + waiting + "}"; + } +} diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/CredentialContext.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/CredentialContext.java new file mode 100644 index 000000000..e5cc23e5f --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/CredentialContext.java @@ -0,0 +1,45 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import java.util.Map; + +/** + * Internal per-call transport for resolved secrets, from {@code WorkerManager} (which + * resolves declared credentials before invoking a handler) to {@code ToolRegistry} + * (which snapshots them into the {@code ToolContext} it builds for the call). + * + *

    Not part of the public API — tool code reads secrets via + * {@code ToolContext.getCredential(...)}, never from here. A {@link ThreadLocal} is used + * (rather than the input map) so secrets never enter task input/output that may be logged + * or serialized. {@code WorkerManager} sets the context immediately before invoking the + * handler and clears it in a {@code finally}, on the same worker thread; concurrent worker + * threads therefore see independent contexts and cannot leak across each other. + */ +public final class CredentialContext { + + private static final ThreadLocal> CURRENT = new ThreadLocal<>(); + + private CredentialContext() {} + + /** Establish the per-call secret context (no-op clear when empty/null). */ + public static void set(Map credentials) { + if (credentials == null || credentials.isEmpty()) { + CURRENT.remove(); + } else { + CURRENT.set(Map.copyOf(credentials)); + } + } + + /** Clear the per-call secret context. Always safe to call. */ + public static void clear() { + CURRENT.remove(); + } + + /** The current call's resolved secrets, or an empty map if none. */ + public static Map current() { + Map ctx = CURRENT.get(); + return ctx == null ? Map.of() : ctx; + } +} diff --git a/sdk/java/src/main/java/ai/agentspan/internal/JsonMapper.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/JsonMapper.java similarity index 97% rename from sdk/java/src/main/java/ai/agentspan/internal/JsonMapper.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/internal/JsonMapper.java index a155659ec..26a635471 100644 --- a/sdk/java/src/main/java/ai/agentspan/internal/JsonMapper.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/JsonMapper.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.internal; +package org.conductoross.conductor.ai.internal; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -10,8 +10,6 @@ import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import java.util.Map; - /** * Singleton ObjectMapper factory with consistent configuration. */ diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/PendingTool.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/PendingTool.java new file mode 100644 index 000000000..ee956090d --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/PendingTool.java @@ -0,0 +1,67 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Details of the HITL task that is currently paused, embedded in {@link AgentStatusResponse}. + * + *

    Present only when {@link AgentStatusResponse#isWaiting()} is {@code true}. + * Pass {@link #getTaskRefName()} back to the server via + * {@link AgentClient#respond(String, java.util.Map)} to resume execution. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class PendingTool { + + @JsonProperty("taskRefName") + private String taskRefName; + + @JsonProperty("tool_name") + private String toolName; + + @JsonProperty("parameters") + private Map parameters; + + @JsonProperty("response_schema") + private Object responseSchema; + + @JsonProperty("response_ui_schema") + private Object responseUiSchema; + + public PendingTool() {} + + /** Conductor task reference name — echoed back in the respond body when needed. */ + public String getTaskRefName() { + return taskRefName; + } + + /** Logical tool name shown to the human reviewer. */ + public String getToolName() { + return toolName; + } + + /** Arguments the agent passed to the tool (what the human is being asked to approve). */ + public Map getParameters() { + return parameters; + } + + /** JSON Schema the response body must conform to, or {@code null} if unconstrained. */ + public Object getResponseSchema() { + return responseSchema; + } + + /** UI rendering hints for approval form rendering, or {@code null} if absent. */ + public Object getResponseUiSchema() { + return responseUiSchema; + } + + @Override + public String toString() { + return "PendingTool{toolName=" + toolName + ", taskRefName=" + taskRefName + "}"; + } +} diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/RespondBody.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/RespondBody.java new file mode 100644 index 000000000..8fc265e93 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/RespondBody.java @@ -0,0 +1,93 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import java.util.LinkedHashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Request body for {@code POST /api/agent/{executionId}/respond}. + * + *

    The server merges this body into the pending HUMAN task's output data. Three + * patterns are supported: + * + *

      + *
    • Approve/reject — standard HITL flows: use {@link #approve()}, + * {@link #approve(String)}, {@link #reject(String)}.
    • + *
    • MANUAL strategy — agent selection: use {@link #of(Map)} with + * e.g. {@code Map.of("selected", "writer")}.
    • + *
    • Custom schema — arbitrary tool response schemas: use + * {@link #of(Map)} with the schema-defined keys.
    • + *
    + * + *

    {@code @JsonAnyGetter} / {@code @JsonAnySetter} flatten {@code extraFields} + * into the top-level JSON object so all fields appear at the root level, matching + * how the server reads the body as a plain {@code Map}. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class RespondBody { + + @JsonProperty("approved") + private final Boolean approved; + + @JsonProperty("reason") + private final String reason; + + /** Arbitrary extra fields serialized at the top level via {@link JsonAnyGetter}. */ + private final Map extraFields; + + private RespondBody(Boolean approved, String reason, Map extraFields) { + this.approved = approved; + this.reason = reason; + this.extraFields = extraFields; + } + + // ── Factories ───────────────────────────────────────────────────────── + + /** Standard approval — sends {@code {"approved": true}}. */ + public static RespondBody approve() { + return new RespondBody(true, null, null); + } + + /** Approval with a human-readable comment — sends {@code {"approved": true, "reason": comment}}. */ + public static RespondBody approve(String comment) { + return new RespondBody(true, comment != null && !comment.isEmpty() ? comment : null, null); + } + + /** Rejection with a reason — sends {@code {"approved": false, "reason": reason}}. */ + public static RespondBody reject(String reason) { + return new RespondBody(false, reason != null && !reason.isEmpty() ? reason : null, null); + } + + /** + * Arbitrary response body — wraps the given map directly. Used for MANUAL strategy + * agent selection ({@code Map.of("selected", "writer")}) and custom HITL schemas. + */ + public static RespondBody of(Map data) { + return new RespondBody(null, null, data != null ? new LinkedHashMap<>(data) : null); + } + + // ── Jackson ─────────────────────────────────────────────────────────── + + @JsonAnyGetter + public Map getExtraFields() { + return extraFields; + } + + @JsonAnySetter + void setExtraField(String key, Object value) { + // no-op: this class is write-only (we never deserialize RespondBody) + } + + @Override + public String toString() { + if (extraFields != null) return "RespondBody" + extraFields; + return "RespondBody{approved=" + approved + ", reason=" + reason + "}"; + } +} diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/SseClient.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/SseClient.java new file mode 100644 index 000000000..787c9bc54 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/SseClient.java @@ -0,0 +1,173 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.conductoross.conductor.ai.model.AgentEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.orkes.conductor.client.ApiClient; +import io.orkes.conductor.client.http.Pair; + +import okhttp3.Call; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * Server-Sent Events (SSE) client for streaming agent events + * ({@code GET /api/agent/stream/{executionId}}). + * + *

    Streams through the shared native Conductor {@link ApiClient} — the request + * is built with {@link ApiClient#buildCall} so it rides the SDK's OkHttp client + * and token-refresh auth interceptor, exactly like every other client. The + * response body is read incrementally; parsed events are placed into a + * {@link LinkedBlockingQueue} and consumed via {@link #nextEvent()}. + */ +public class SseClient implements AutoCloseable { + private static final Logger logger = LoggerFactory.getLogger(SseClient.class); + + /** Sentinel value to signal end-of-stream. */ + private static final AgentEvent DONE_SENTINEL = new AgentEvent(null, null, null, null, null, null, "", null, null); + + private final ApiClient apiClient; + private final String executionId; + private final BlockingQueue eventQueue = new LinkedBlockingQueue<>(); + private final AtomicBoolean closed = new AtomicBoolean(false); + private volatile Call call; + + public SseClient(ApiClient apiClient, String executionId) { + this.apiClient = apiClient; + this.executionId = executionId; + } + + /** Connect and start receiving SSE events in a background thread. */ + public void connect() { + Thread streamThread = new Thread(this::streamLoop, "agentspan-sse-" + executionId); + streamThread.setDaemon(true); + streamThread.start(); + } + + /** + * Block until the next event is available and return it. + * + * @return the next event, or null if the stream is done + */ + public AgentEvent nextEvent() { + try { + AgentEvent event = eventQueue.take(); + return event == DONE_SENTINEL ? null : event; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + } + + @Override + public void close() { + closed.set(true); + Call c = call; + if (c != null) c.cancel(); + // Wake up any blocked nextEvent() calls + eventQueue.offer(DONE_SENTINEL); + } + + private void streamLoop() { + StringBuilder dataBuffer = new StringBuilder(); + String[] eventTypeHolder = {null}; + + try { + Map headers = new HashMap<>(); + headers.put("Accept", "text/event-stream"); + headers.put("Cache-Control", "no-cache"); + + // Relative to the ApiClient's base path (the server's /api root); auth + // and token refresh are applied by the client's OkHttp interceptor. + call = apiClient.buildCall( + "/agent/stream/" + executionId, + "GET", + Collections.emptyList(), + Collections.emptyList(), + null, + headers); + + try (Response response = call.execute()) { + if (!response.isSuccessful()) { + if (!closed.get()) { + logger.error("SSE connection failed with status {}", response.code()); + } + return; + } + ResponseBody body = response.body(); + if (body == null) return; + + BufferedReader reader = + new BufferedReader(new InputStreamReader(body.byteStream(), StandardCharsets.UTF_8)); + String rawLine; + while (!closed.get() && (rawLine = reader.readLine()) != null) { + // Strip trailing \r if present + String line = rawLine.endsWith("\r") ? rawLine.substring(0, rawLine.length() - 1) : rawLine; + + if (line.isEmpty()) { + String data = dataBuffer.toString().trim(); + if (!data.isEmpty()) dispatchEvent(eventTypeHolder[0], data); + dataBuffer.setLength(0); + eventTypeHolder[0] = null; + continue; + } + if (line.startsWith(":")) { + continue; // comment / heartbeat + } + if (line.startsWith("event:")) { + eventTypeHolder[0] = line.substring(6).trim(); + } else if (line.startsWith("id:")) { + // Last event ID — tracked but not used currently + } else if (line.startsWith("data:")) { + String dataChunk = line.substring(5); + if (dataChunk.startsWith(" ")) dataChunk = dataChunk.substring(1); + if (dataBuffer.length() > 0) dataBuffer.append("\n"); + dataBuffer.append(dataChunk); + } + } + + // Dispatch any remaining buffered data + String data = dataBuffer.toString().trim(); + if (!data.isEmpty()) dispatchEvent(eventTypeHolder[0], data); + } + } catch (Exception e) { + if (!closed.get()) { + logger.error("SSE stream error: {}", e.getMessage(), e); + } + } finally { + eventQueue.offer(DONE_SENTINEL); + } + } + + @SuppressWarnings("unchecked") + private void dispatchEvent(String eventType, String data) { + try { + if ("[DONE]".equals(data)) { + eventQueue.offer(DONE_SENTINEL); + return; + } + Map parsed = JsonMapper.fromJson(data, Map.class); + AgentEvent event = AgentEvent.fromMap(parsed); + eventQueue.offer(event); + if (event.getType() != null && "done".equals(event.getType().toJsonValue())) { + eventQueue.offer(DONE_SENTINEL); + } + } catch (Exception e) { + logger.warn("Failed to parse SSE event data: {} — {}", data, e.getMessage()); + } + } +} diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/StartResponse.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/StartResponse.java new file mode 100644 index 000000000..12a63adba --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/StartResponse.java @@ -0,0 +1,58 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response from {@code POST /api/agent/deploy} and {@code POST /api/agent/start}. + * + *

    For deploy, {@link #getExecutionId()} is {@code null} — no execution was started. + * For start, {@link #getExecutionId()} is the Conductor workflow ID to pass to + * {@link AgentClient#getAgentStatus(String)} and {@link AgentClient#respond(String, java.util.Map)}. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class StartResponse { + + /** Current canonical field name. {@code @JsonAlias} handles older server versions. */ + @JsonProperty("executionId") + @JsonAlias({"workflowId", "id", "correlationId"}) + private String executionId; + + @JsonProperty("agentName") + private String agentName; + + @JsonProperty("requiredWorkers") + private List requiredWorkers; + + public StartResponse() {} + + /** Conductor workflow ID for this execution, or {@code null} for deploy-only calls. */ + public String getExecutionId() { + return executionId; + } + + /** The registered workflow name on the server. */ + public String getAgentName() { + return agentName; + } + + /** + * Task type names the SDK must have workers polling before the agent can progress. + * Handled automatically by the runtime. + */ + public List getRequiredWorkers() { + return requiredWorkers != null ? requiredWorkers : Collections.emptyList(); + } + + @Override + public String toString() { + return "StartResponse{executionId=" + executionId + ", agentName=" + agentName + "}"; + } +} diff --git a/sdk/java/src/main/java/ai/agentspan/internal/ToolRegistry.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/ToolRegistry.java similarity index 82% rename from sdk/java/src/main/java/ai/agentspan/internal/ToolRegistry.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/internal/ToolRegistry.java index 7d732b977..f3616901c 100644 --- a/sdk/java/src/main/java/ai/agentspan/internal/ToolRegistry.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/ToolRegistry.java @@ -1,15 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.internal; - -import ai.agentspan.annotations.GuardrailDef; -import ai.agentspan.annotations.Tool; -import ai.agentspan.model.GuardrailResult; -import ai.agentspan.model.ToolContext; -import ai.agentspan.model.ToolDef; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +package org.conductoross.conductor.ai.internal; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @@ -28,6 +20,14 @@ import java.util.Map; import java.util.function.Function; +import org.conductoross.conductor.ai.annotations.GuardrailDef; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.model.GuardrailResult; +import org.conductoross.conductor.ai.model.ToolContext; +import org.conductoross.conductor.ai.model.ToolDef; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Discovers {@link Tool} and {@link GuardrailDef} annotated methods via reflection. */ @@ -65,7 +65,7 @@ public static List fromInstance(Object obj) { } } - ToolContext context = new ToolContext(null, null, null, agentState); + ToolContext context = new ToolContext(null, null, null, agentState, CredentialContext.current()); Object[] methodArgs = buildMethodArgs(method, inputData, context); Object result = method.invoke(obj, methodArgs); @@ -92,20 +92,20 @@ public static List fromInstance(Object obj) { Map outputSchema = typeToJsonSchema(method.getReturnType()); tools.add(new ToolDef.Builder() - .name(name) - .description(ann.description()) - .inputSchema(schema) - .outputSchema(outputSchema) - .func(func) - .approvalRequired(ann.approvalRequired()) - .timeoutSeconds(ann.timeoutSeconds()) - .maxCalls(ann.maxCalls()) - .retryCount(ann.retryCount()) - .retryDelaySeconds(ann.retryDelaySeconds()) - .retryPolicy(ann.retryPolicy()) - .toolType("worker") - .credentials(credentials) - .build()); + .name(name) + .description(ann.description()) + .inputSchema(schema) + .outputSchema(outputSchema) + .func(func) + .approvalRequired(ann.approvalRequired()) + .timeoutSeconds(ann.timeoutSeconds()) + .maxCalls(ann.maxCalls()) + .retryCount(ann.retryCount()) + .retryDelaySeconds(ann.retryDelaySeconds()) + .retryPolicy(ann.retryPolicy()) + .toolType("worker") + .credentials(credentials) + .build()); logger.debug("Registered tool '{}' from {}", name, obj.getClass().getSimpleName()); } @@ -116,10 +116,10 @@ public static List fromInstance(Object obj) { * Discover all {@link GuardrailDef}-annotated methods on an object and return guardrail definitions. * * @param obj the object to inspect - * @return list of ai.agentspan.model.GuardrailDef instances + * @return list of org.conductoross.conductor.ai.model.GuardrailDef instances */ - public static List guardrailsFromInstance(Object obj) { - List guardrails = new ArrayList<>(); + public static List guardrailsFromInstance(Object obj) { + List guardrails = new ArrayList<>(); for (Method method : obj.getClass().getMethods()) { GuardrailDef ann = method.getAnnotation(GuardrailDef.class); if (ann == null) continue; @@ -135,16 +135,17 @@ public static List guardrailsFromInstance(Objec } }; - guardrails.add(new ai.agentspan.model.GuardrailDef.Builder() - .name(name) - .position(ann.position()) - .onFail(ann.onFail()) - .maxRetries(ann.maxRetries()) - .func(func) - .guardrailType("custom") - .build()); - - logger.debug("Registered guardrail '{}' from {}", name, obj.getClass().getSimpleName()); + guardrails.add(new org.conductoross.conductor.ai.model.GuardrailDef.Builder() + .name(name) + .position(ann.position()) + .onFail(ann.onFail()) + .maxRetries(ann.maxRetries()) + .func(func) + .guardrailType("custom") + .build()); + + logger.debug( + "Registered guardrail '{}' from {}", name, obj.getClass().getSimpleName()); } return guardrails; } @@ -206,19 +207,19 @@ public static Map typeToJsonSchema(Class type) { Map schema = new LinkedHashMap<>(); if (type == String.class) { schema.put("type", "string"); - } else if (type == int.class || type == Integer.class - || type == long.class || type == Long.class) { + } else if (type == int.class || type == Integer.class || type == long.class || type == Long.class) { schema.put("type", "integer"); - } else if (type == double.class || type == Double.class - || type == float.class || type == Float.class) { + } else if (type == double.class || type == Double.class || type == float.class || type == Float.class) { schema.put("type", "number"); } else if (type == boolean.class || type == Boolean.class) { schema.put("type", "boolean"); } else if (type == LocalDate.class) { schema.put("type", "string"); schema.put("format", "date"); - } else if (type == Instant.class || type == LocalDateTime.class - || type == OffsetDateTime.class || type == ZonedDateTime.class) { + } else if (type == Instant.class + || type == LocalDateTime.class + || type == OffsetDateTime.class + || type == ZonedDateTime.class) { schema.put("type", "string"); schema.put("format", "date-time"); } else if (type == Duration.class) { @@ -255,8 +256,8 @@ private static Object[] buildMethodArgs(Method method, Map input if (inputData != null && !inputData.isEmpty()) { // Check if params have real names (compiled with -parameters) boolean hasRealNames = params.length > 0 - && !params[0].getName().equals("arg0") - && !params[0].getName().startsWith("arg"); + && !params[0].getName().equals("arg0") + && !params[0].getName().startsWith("arg"); if (!hasRealNames) { // Fall back to positional: use values in iteration order @@ -313,10 +314,10 @@ private static Object coerce(Object value, Class targetType, Type genericType if (List.class.isAssignableFrom(targetType)) { try { com.fasterxml.jackson.databind.type.TypeFactory tf = - JsonMapper.get().getTypeFactory(); + JsonMapper.get().getTypeFactory(); com.fasterxml.jackson.databind.JavaType jt = (genericType != null) - ? tf.constructType(genericType) - : tf.constructCollectionType(List.class, Object.class); + ? tf.constructType(genericType) + : tf.constructCollectionType(List.class, Object.class); return JsonMapper.get().convertValue(value, jt); } catch (Exception e) { return value; @@ -346,8 +347,8 @@ private static Object coerce(Object value, Class targetType, Type genericType // Fallback: try Jackson conversion for complex types try { com.fasterxml.jackson.databind.JavaType jt = (genericType != null) - ? JsonMapper.get().getTypeFactory().constructType(genericType) - : JsonMapper.get().getTypeFactory().constructType(targetType); + ? JsonMapper.get().getTypeFactory().constructType(genericType) + : JsonMapper.get().getTypeFactory().constructType(targetType); return JsonMapper.get().convertValue(value, jt); } catch (Exception e) { return value; diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/WorkerCredentialFetcher.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/WorkerCredentialFetcher.java new file mode 100644 index 000000000..49d0f245a --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/WorkerCredentialFetcher.java @@ -0,0 +1,95 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.conductoross.conductor.ai.exceptions.CredentialAuthException; +import org.conductoross.conductor.ai.exceptions.CredentialNotFoundException; +import org.conductoross.conductor.ai.exceptions.CredentialRateLimitException; +import org.conductoross.conductor.ai.exceptions.CredentialServiceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.netflix.conductor.client.exception.ConductorClientException; +import com.netflix.conductor.client.http.ConductorClient; +import com.netflix.conductor.client.http.ConductorClientRequest; +import com.netflix.conductor.client.http.ConductorClientRequest.Method; + +/** + * Resolves declared secret values from the AgentSpan server ({@code POST + * /api/workers/secrets}) using a worker execution token, over the shared native + * Conductor {@link ConductorClient}/ApiClient (same HTTP + token-auth backend as + * every other client). Mirrors Python's {@code WorkerCredentialFetcher}. + * + *

    Java is tier-1-only per {@code docs/design/secret-injection-contract.md} §6 + * rule 1: {@code System.getenv()} is immutable at runtime. The fetcher returns + * values to the caller, who passes them to tool handlers via + * {@code ToolContext#getCredential}. + * + *

    Error contract — every failure mode produces a typed exception. Conductor's + * {@link ConductorClientException} (raised on non-2xx) is mapped by HTTP status. + */ +public class WorkerCredentialFetcher { + + private static final Logger logger = LoggerFactory.getLogger(WorkerCredentialFetcher.class); + + private static final TypeReference> SECRETS_TYPE = new TypeReference>() {}; + + private final ConductorClient client; + + public WorkerCredentialFetcher(ConductorClient client) { + this.client = client; + } + + /** + * Resolve {@code names} via {@code POST /api/workers/secrets} using + * {@code executionToken}. + * + * @throws CredentialNotFoundException token absent, or server returned 200 + * with some names missing + * @throws CredentialAuthException token rejected (401) + * @throws CredentialRateLimitException 429 + * @throws CredentialServiceException 5xx/4xx or network failure + */ + public Map fetch(String executionToken, List names) { + if (names == null || names.isEmpty()) return Collections.emptyMap(); + if (executionToken == null || executionToken.isBlank()) { + throw new CredentialNotFoundException(names); + } + + ConductorClientRequest request = ConductorClientRequest.builder() + .method(Method.POST) + .path("/workers/secrets") + .body(Map.of("token", executionToken, "names", names)) + .build(); + + Map resolved; + try { + resolved = client.execute(request, SECRETS_TYPE).getData(); + } catch (ConductorClientException e) { + int status = e.getStatus(); + if (status == 401) throw new CredentialAuthException(e.getMessage()); + if (status == 429) throw new CredentialRateLimitException(); + logger.error("Credential service error ({}): {}", status, e.getMessage()); + throw new CredentialServiceException(status, e.getMessage()); + } + if (resolved == null) resolved = new LinkedHashMap<>(); + + List missing = new ArrayList<>(); + for (String name : names) { + if (!resolved.containsKey(name)) missing.add(name); + } + if (!missing.isEmpty()) { + logger.error("Credentials not found on server: {}", missing); + throw new CredentialNotFoundException(missing); + } + return resolved; + } +} diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/WorkerManager.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/WorkerManager.java new file mode 100644 index 000000000..4c8444433 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/internal/WorkerManager.java @@ -0,0 +1,440 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import org.conductoross.conductor.ai.AgentConfig; +import org.conductoross.conductor.ai.exceptions.CredentialAuthException; +import org.conductoross.conductor.ai.exceptions.CredentialNotFoundException; +import org.conductoross.conductor.ai.exceptions.CredentialRateLimitException; +import org.conductoross.conductor.ai.exceptions.CredentialServiceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.netflix.conductor.client.automator.TaskRunnerConfigurer; +import com.netflix.conductor.client.http.ConductorClient; +import com.netflix.conductor.client.http.MetadataClient; +import com.netflix.conductor.client.http.TaskClient; +import com.netflix.conductor.client.worker.Worker; +import com.netflix.conductor.common.metadata.tasks.Task; +import com.netflix.conductor.common.metadata.tasks.TaskDef; +import com.netflix.conductor.common.metadata.tasks.TaskResult; + +/** + * Manages worker task handlers and drives them with the official Conductor + * client ({@link TaskRunnerConfigurer} + {@link Worker}). + * + *

    Replaces the previous hand-rolled poll loop. The Conductor client provides + * battle-tested task polling, backoff, managed worker threads, and — crucially — + * automatic lease extension (heartbeat) for in-flight tasks (every worker + * here returns {@code leaseExtendEnabled() == true}). A handler that blocks for + * minutes keeps its lease alive instead of being reclaimed and re-dispatched. + * + *

    Agentspan registers workers incrementally (per run, sometimes under a + * per-execution domain), whereas a {@link TaskRunnerConfigurer} is built from a + * fixed worker set. We bridge the two models by (re)building the configurer in + * {@link #startAll()} whenever a new task type has been registered since + * the last build. The common cases — {@code serve()} and repeated runs of the + * same agent — build exactly once; re-registering an existing task only swaps the + * handler (looked up live in {@link Worker#execute}) and needs no rebuild. + * + *

    What is preserved from the old implementation: per-task worker domains + * ({@link TaskRunnerConfigurer.Builder#withTaskToDomain}), declared-credential + * resolution before each handler call (with terminal failure on credential + * errors so Conductor doesn't burn retries), and the {@code result → outputData} + * mapping. + */ +public class WorkerManager { + private static final Logger logger = LoggerFactory.getLogger(WorkerManager.class); + + /** + * Minimum threads per worker type in the shared pool. Each worker type needs at least one + * thread to make progress, so the actual floor is {@code max(MIN_THREADS_PER_WORKER × N, configured)}. + * Keeping this at 1 means the configured {@link AgentConfig#getWorkerThreadCount()} is always + * respected (a test can set 1; production callers set higher). + */ + private static final int MIN_THREADS_PER_WORKER = 1; + + /** Default task-def timeout (seconds) for handlers with no configured timeout. */ + static final int DEFAULT_TASK_TIMEOUT_SECONDS = 300; + + /** + * Slack added on top of a handler's configured timeout so the server's + * patience always exceeds the worker's blocking duration — covering process + * kill/teardown (e.g. Docker's {@code timeout + 10}s) plus the task-update + * round-trip. With lease extension this is belt-and-suspenders, but it keeps + * the registered task def honest. + */ + static final int TASK_TIMEOUT_SLACK_SECONDS = 60; + + /** + * Effective task-def timeout for a handler that blocks up to + * {@code configuredSeconds}. Floors at {@link #DEFAULT_TASK_TIMEOUT_SECONDS} + * so short tasks keep the proven-safe default, and only ever raises the + * ceiling for genuinely long-running handlers — the server's + * {@code responseTimeoutSeconds} can never drift below the handler's timeout. + */ + static int effectiveTaskTimeout(int configuredSeconds) { + if (configuredSeconds <= 0) return DEFAULT_TASK_TIMEOUT_SECONDS; + return Math.max(DEFAULT_TASK_TIMEOUT_SECONDS, configuredSeconds + TASK_TIMEOUT_SLACK_SECONDS); + } + + private final AgentConfig config; + private final WorkerCredentialFetcher credentialFetcher; + private final TaskClient taskClient; + private final MetadataClient metadataClient; + + private final ConcurrentHashMap, Object>> handlers; + /** Optional worker domain per task name. Tasks without an entry poll the default queue. */ + private final ConcurrentHashMap taskDomains; + /** Declared credential names per task name. Empty list when no secrets are declared. */ + private final ConcurrentHashMap> taskCredentials; + + /** + * Domain applied by the no-arg {@link #register(String, Function)} overload. + * AgentRuntime sets this for the lifetime of a single + * {@code prepareWorkers(agent, domain)} call so all subsequent register + * calls register under the same per-execution domain. + */ + private volatile String currentDomain; + + private final Object lifecycleLock = new Object(); + private TaskRunnerConfigurer configurer; + /** True when a new task type was registered since the last configurer build. */ + private boolean workerSetChanged; + + public WorkerManager(AgentConfig config, ConductorClient conductorClient) { + this.config = config; + this.credentialFetcher = new WorkerCredentialFetcher(conductorClient); + this.handlers = new ConcurrentHashMap<>(); + this.taskDomains = new ConcurrentHashMap<>(); + this.taskCredentials = new ConcurrentHashMap<>(); + + // Worker protocol via the shared native Conductor client. + this.taskClient = new TaskClient(conductorClient); + this.metadataClient = new MetadataClient(conductorClient); + } + + // ── Registration ─────────────────────────────────────────────────────── + + /** + * Register a task handler function for the given task name. + * + * @param taskName the Conductor task type name + * @param handler the function to call when a task is polled + */ + public void register(String taskName, Function, Object> handler) { + register(taskName, handler, currentDomain); + } + + /** + * Set the domain that the no-arg {@link #register(String, Function)} + * overload will apply to subsequent calls. Pass {@code null} to clear. + */ + public void setCurrentDomain(String domain) { + this.currentDomain = domain; + } + + /** Read the domain set by the most recent {@link #setCurrentDomain(String)}. */ + public String getCurrentDomain() { + return currentDomain; + } + + // ── Test hooks (package-private) ───────────────────────────────────────── + + /** Visible for testing: the thread count that would be used if startAll() were called now. */ + int computeThreadCount() { + return Math.max(config.getWorkerThreadCount(), MIN_THREADS_PER_WORKER * handlers.size()); + } + + /** Visible for testing: the domain a task is currently registered under (or null). */ + String getTaskDomain(String taskName) { + return taskDomains.get(taskName); + } + + /** Visible for testing: whether a runner (re)build is pending. */ + boolean isWorkerSetChanged() { + synchronized (lifecycleLock) { + return workerSetChanged; + } + } + + /** Visible for testing: simulate {@link #startAll()} having consumed the change flag. */ + void clearWorkerSetChangedForTest() { + synchronized (lifecycleLock) { + workerSetChanged = false; + } + } + + public void register(String taskName, Function, Object> handler, String domain) { + register(taskName, handler, domain, Collections.emptyList()); + } + + public void register( + String taskName, Function, Object> handler, String domain, List credentials) { + register(taskName, handler, domain, credentials, 0); + } + + /** + * Register a task handler whose Conductor task-def timeout tracks the + * handler's configured blocking timeout. + * + *

    {@code taskTimeoutSeconds} is the handler's max blocking time (e.g. + * {@code CliConfig.timeout}, the code-execution timeout, or a worker tool's + * {@code timeoutSeconds}). The registered task def's + * {@code responseTimeoutSeconds} is set to {@link #effectiveTaskTimeout} + * of it, so the server's patience never drifts below the handler's timeout + * (and, combined with lease extension, a long task is not reclaimed + * mid-flight). Pass {@code 0} for the default. + */ + public void register( + String taskName, + Function, Object> handler, + String domain, + List credentials, + int taskTimeoutSeconds) { + boolean isNew = !handlers.containsKey(taskName); + String previousDomain = taskDomains.get(taskName); + handlers.put(taskName, handler); + + String normalizedDomain = (domain != null && !domain.isEmpty()) ? domain : null; + if (normalizedDomain != null) { + taskDomains.put(taskName, normalizedDomain); + } else { + taskDomains.remove(taskName); + } + if (credentials != null && !credentials.isEmpty()) { + taskCredentials.put(taskName, List.copyOf(credentials)); + } else { + taskCredentials.remove(taskName); + } + + if (!isNew) { + // Same task type — the Worker looks the handler up live, so a swapped + // handler takes effect with no rebuild. BUT the domain is baked into the + // running configurer's taskToDomain at build time, so a *changed* domain + // (e.g. a stateful run's per-execution runId registered after a prior + // no-domain registration) requires a rebuild — otherwise the worker keeps + // polling the old/default queue while the server enqueues the task under + // the new domain, and the task sits SCHEDULED until the run times out. + if (!Objects.equals(previousDomain, normalizedDomain)) { + synchronized (lifecycleLock) { + workerSetChanged = true; + } + logger.info( + "Re-registered worker for task {} under new domain {} (was {})", + taskName, + normalizedDomain, + previousDomain); + } else { + logger.debug("Re-registered handler for task: {} (domain={})", taskName, domain); + } + return; + } + + // Size and upsert the task def so the server's timeouts track the handler. + registerTaskDef(taskName, taskTimeoutSeconds); + + synchronized (lifecycleLock) { + workerSetChanged = true; + } + logger.info("Registered worker for task: {} (domain={})", taskName, domain); + } + + private void registerTaskDef(String taskName, int configuredTimeoutSeconds) { + try { + long timeout = effectiveTaskTimeout(configuredTimeoutSeconds); + TaskDef taskDef = new TaskDef(taskName); + taskDef.setTimeoutSeconds(timeout); + taskDef.setResponseTimeoutSeconds(timeout); + metadataClient.registerTaskDefs(List.of(taskDef)); + } catch (Exception e) { + logger.debug("Could not register task def {} (may already exist): {}", taskName, e.getMessage()); + } + } + + // ── Lifecycle ────────────────────────────────────────────────────────── + + /** + * Start (or rebuild) the Conductor task runner for all registered workers. + * Idempotent: returns immediately when no new task type has been registered + * since the last build. + */ + public void startAll() { + synchronized (lifecycleLock) { + if (configurer != null && !workerSetChanged) { + return; // already running, nothing new to add + } + if (handlers.isEmpty()) { + return; // nothing to run yet + } + + if (configurer != null) { + // A new task type appeared — rebuild with the full worker set. + try { + configurer.shutdown(); + } catch (Exception e) { + logger.debug("Error shutting down previous task runner: {}", e.getMessage()); + } + configurer = null; + } + + List workers = new ArrayList<>(); + for (String taskName : handlers.keySet()) { + workers.add(makeWorker(taskName)); + } + + Map taskToDomain = new HashMap<>(); + for (Map.Entry e : taskDomains.entrySet()) { + if (e.getValue() != null && !e.getValue().isEmpty()) { + taskToDomain.put(e.getKey(), e.getValue()); + } + } + + // Need at least 1 thread per worker type (otherwise a blocking handler starves others), + // but respect the configured count — don't silently override an explicit setting. + int threadCount = Math.max(config.getWorkerThreadCount(), MIN_THREADS_PER_WORKER * workers.size()); + + TaskRunnerConfigurer.Builder builder = new TaskRunnerConfigurer.Builder(taskClient, workers) + .withThreadCount(threadCount) + .withWorkerNamePrefix("agentspan-worker-"); + if (!taskToDomain.isEmpty()) { + builder.withTaskToDomain(taskToDomain); + } + + configurer = builder.build(); + configurer.init(); + workerSetChanged = false; + logger.info("Started Conductor task runner: {} worker(s), {} thread(s)", workers.size(), threadCount); + } + } + + /** Stop the Conductor task runner. */ + public void stop() { + synchronized (lifecycleLock) { + if (configurer != null) { + try { + configurer.shutdown(); + } catch (Exception e) { + logger.debug("Error during task runner shutdown: {}", e.getMessage()); + } + configurer = null; + } + } + } + + // ── Worker ───────────────────────────────────────────────────────────── + + /** + * Build a Conductor {@link Worker} for {@code taskName}. The handler is + * looked up live so a re-registered handler takes effect without a rebuild. + */ + private Worker makeWorker(String taskName) { + return new Worker() { + @Override + public String getTaskDefName() { + return taskName; + } + + @Override + public int getPollingInterval() { + return config.getWorkerPollIntervalMs(); + } + + @Override + public boolean leaseExtendEnabled() { + // Heartbeat: keep a long-running task's lease alive so the server + // does not reclaim and re-dispatch it while the handler blocks. + return true; + } + + @Override + public TaskResult execute(Task task) { + return executeHandler(taskName, task); + } + }; + } + + private TaskResult executeHandler(String taskName, Task task) { + TaskResult result = new TaskResult(task); + Map inputData = task.getInputData() != null ? task.getInputData() : Collections.emptyMap(); + + // Resolve declared secrets BEFORE invoking the handler. Credential + // failures are terminal so Conductor doesn't burn retries on a config + // problem. See docs/design/secret-injection-contract.md. + Map resolvedSecrets = Collections.emptyMap(); + List declared = taskCredentials.getOrDefault(taskName, Collections.emptyList()); + if (!declared.isEmpty()) { + String execToken = extractExecutionToken(inputData); + try { + resolvedSecrets = credentialFetcher.fetch(execToken, declared); + } catch (CredentialNotFoundException + | CredentialAuthException + | CredentialRateLimitException + | CredentialServiceException ce) { + logger.error( + "Credential resolution failed for task {} ({}): {}", + taskName, + task.getTaskId(), + ce.getMessage()); + result.setStatus(TaskResult.Status.FAILED_WITH_TERMINAL_ERROR); + result.setReasonForIncompletion("Credential resolution failed: " + ce.getMessage()); + return result; + } + } + + Function, Object> handler = handlers.get(taskName); + if (handler == null) { + result.setStatus(TaskResult.Status.FAILED); + result.setReasonForIncompletion("No handler registered for task " + taskName); + return result; + } + + try { + CredentialContext.set(resolvedSecrets); + try { + Object out = handler.apply(inputData); + result.setStatus(TaskResult.Status.COMPLETED); + result.setOutputData(buildOutput(out)); + logger.debug("Completed task {} ({})", taskName, task.getTaskId()); + } finally { + CredentialContext.clear(); + } + } catch (Exception e) { + logger.error("Task {} ({}) failed: {}", taskName, task.getTaskId(), e.getMessage(), e); + result.setStatus(TaskResult.Status.FAILED); + result.setReasonForIncompletion(e.getMessage()); + } + return result; + } + + /** + * Pull the execution token out of {@code inputData["__agentspan_ctx__"]["execution_token"]}. + * Returns {@code null} if no token is present. + */ + @SuppressWarnings("unchecked") + private static String extractExecutionToken(Map inputData) { + if (inputData == null) return null; + Object ctx = inputData.get("__agentspan_ctx__"); + if (!(ctx instanceof Map ctxMap)) return null; + Object token = ctxMap.get("execution_token"); + if (token == null) token = ctxMap.get("executionToken"); // tolerate camelCase + return token instanceof String s ? s : null; + } + + @SuppressWarnings("unchecked") + private Map buildOutput(Object result) { + if (result == null) return Map.of(); + if (result instanceof Map) return (Map) result; + return Map.of("result", result); + } +} diff --git a/sdk/java/src/main/java/ai/agentspan/model/AgentEvent.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/AgentEvent.java similarity index 69% rename from sdk/java/src/main/java/ai/agentspan/model/AgentEvent.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/model/AgentEvent.java index 8886bc466..1ab1e9a6b 100644 --- a/sdk/java/src/main/java/ai/agentspan/model/AgentEvent.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/AgentEvent.java @@ -1,9 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.model; - -import ai.agentspan.enums.EventType; +package org.conductoross.conductor.ai.model; import java.util.Arrays; import java.util.HashSet; @@ -11,6 +9,8 @@ import java.util.Map; import java.util.Set; +import org.conductoross.conductor.ai.enums.EventType; + /** * A single event from a streaming agent execution. */ @@ -46,23 +46,48 @@ public AgentEvent( this.target = target; } - public EventType getType() { return type; } - public String getContent() { return content; } - public String getToolName() { return toolName; } - public Map getArgs() { return args; } - public Object getResult() { return result; } - public Object getOutput() { return output; } - public String getExecutionId() { return executionId; } - public String getGuardrailName() { return guardrailName; } - public String getTarget() { return target; } + public EventType getType() { + return type; + } + + public String getContent() { + return content; + } + + public String getToolName() { + return toolName; + } + + public Map getArgs() { + return args; + } + + public Object getResult() { + return result; + } + + public Object getOutput() { + return output; + } + + public String getExecutionId() { + return executionId; + } + + public String getGuardrailName() { + return guardrailName; + } + + public String getTarget() { + return target; + } /** * Create an AgentEvent from a raw map (as parsed from SSE JSON). */ /** Internal keys injected by the server that should not be shown as tool arguments. */ - private static final Set INTERNAL_KEYS = new HashSet<>(Arrays.asList( - "__agentspan_ctx__", "_agent_state", "method" - )); + private static final Set INTERNAL_KEYS = + new HashSet<>(Arrays.asList("__agentspan_ctx__", "_agent_state", "method")); @SuppressWarnings("unchecked") public static AgentEvent fromMap(Map data) { @@ -97,16 +122,15 @@ public static AgentEvent fromMap(Map data) { } return new AgentEvent( - type, - (String) data.get("content"), - (String) data.get("toolName"), - cleanArgs, - data.get("result"), - data.get("output"), - (String) data.getOrDefault("executionId", ""), - (String) data.get("guardrailName"), - (String) data.get("target") - ); + type, + (String) data.get("content"), + (String) data.get("toolName"), + cleanArgs, + data.get("result"), + data.get("output"), + (String) data.getOrDefault("executionId", ""), + (String) data.get("guardrailName"), + (String) data.get("target")); } @Override diff --git a/sdk/java/src/main/java/ai/agentspan/model/AgentHandle.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/AgentHandle.java similarity index 51% rename from sdk/java/src/main/java/ai/agentspan/model/AgentHandle.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/model/AgentHandle.java index bb56f74e0..07a387072 100644 --- a/sdk/java/src/main/java/ai/agentspan/model/AgentHandle.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/AgentHandle.java @@ -1,23 +1,28 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.model; - -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.internal.HttpApi; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +package org.conductoross.conductor.ai.model; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; + +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.internal.AgentClient; +import org.conductoross.conductor.ai.internal.AgentStatusResponse; +import org.conductoross.conductor.ai.internal.RespondBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.netflix.conductor.client.http.WorkflowClient; +import com.netflix.conductor.common.metadata.tasks.Task; +import com.netflix.conductor.common.run.Workflow; /** * A handle to a running agent workflow. * - *

    Returned by {@link ai.agentspan.AgentRuntime#start(ai.agentspan.Agent, String)}. + *

    Returned by {@link org.conductoross.conductor.ai.AgentRuntime#start(org.conductoross.conductor.ai.Agent, String)}. * Allows checking status, interacting with human-in-the-loop pauses, and controlling * execution — from any process, even after restarts. */ @@ -28,11 +33,13 @@ public class AgentHandle { private static final long DEFAULT_TIMEOUT_MS = 600_000; // 10 minutes private final String executionId; - private final HttpApi httpApi; + private final AgentClient agentClient; + private final WorkflowClient workflowClient; - public AgentHandle(String executionId, HttpApi httpApi) { + public AgentHandle(String executionId, AgentClient agentClient, WorkflowClient workflowClient) { this.executionId = executionId; - this.httpApi = httpApi; + this.agentClient = agentClient; + this.workflowClient = workflowClient; } public String getExecutionId() { @@ -56,14 +63,23 @@ public AgentResult waitForResult() { * @param pollIntervalMs polling interval in milliseconds * @return the agent result */ + // Consecutive poll errors before we escalate from DEBUG→WARN→ERROR logging. + private static final int POLL_ERROR_WARN_AT = 3; + + private static final int POLL_ERROR_FAIL_AT = 10; + @SuppressWarnings("unchecked") public AgentResult waitForResult(long timeoutMs, long pollIntervalMs) { long startTime = System.currentTimeMillis(); + int consecutiveErrors = 0; + Exception lastError = null; while (System.currentTimeMillis() - startTime < timeoutMs) { try { - Map status = httpApi.getAgentStatus(executionId); - String workflowStatus = (String) status.get("status"); + AgentStatusResponse status = agentClient.getAgentStatus(executionId); + consecutiveErrors = 0; // reset on success + lastError = null; + String workflowStatus = status.getStatus(); if (workflowStatus == null) { logger.debug("Waiting for agent {} — status unknown", executionId); @@ -78,7 +94,24 @@ public AgentResult waitForResult(long timeoutMs, long pollIntervalMs) { Thread.currentThread().interrupt(); throw new RuntimeException("Interrupted while waiting for agent result", e); } catch (Exception e) { - logger.error("Error polling agent status: {}", e.getMessage()); + lastError = e; + consecutiveErrors++; + if (consecutiveErrors >= POLL_ERROR_FAIL_AT) { + // Too many consecutive failures — the server is unhealthy. Surface the error + // rather than silently timing out, which hides the root cause for 10 minutes. + throw new RuntimeException( + "Giving up polling agent " + executionId + " after " + consecutiveErrors + + " consecutive errors (last: " + e.getMessage() + ")", + e); + } else if (consecutiveErrors >= POLL_ERROR_WARN_AT) { + logger.warn( + "Repeated errors polling agent {} ({} consecutive): {}", + executionId, + consecutiveErrors, + e.getMessage()); + } else { + logger.debug("Error polling agent status (attempt {}): {}", consecutiveErrors, e.getMessage()); + } try { Thread.sleep(pollIntervalMs); } catch (InterruptedException ie) { @@ -88,14 +121,22 @@ public AgentResult waitForResult(long timeoutMs, long pollIntervalMs) { } } - throw new RuntimeException("Agent timed out after " + timeoutMs + "ms: " + executionId); + String lastErrorMsg = lastError != null ? " (last poll error: " + lastError.getMessage() + ")" : ""; + throw new RuntimeException("Agent timed out after " + timeoutMs + "ms: " + executionId + lastErrorMsg); + } + + /** Approve a pending tool call that requires human approval. */ + public void approve() { + agentClient.respond(executionId, RespondBody.approve()); } /** - * Approve a pending tool call that requires human approval. + * Approve with a human-readable comment. + * + * @param comment optional comment sent alongside the approval */ - public void approve() { - httpApi.respond(executionId, approveBody(null)); + public void approve(String comment) { + agentClient.respond(executionId, RespondBody.approve(comment)); } /** @@ -104,10 +145,7 @@ public void approve() { * @param reason rejection reason */ public void reject(String reason) { - Map body = new java.util.HashMap<>(); - body.put("approved", false); - if (reason != null && !reason.isEmpty()) body.put("reason", reason); - httpApi.respond(executionId, body); + agentClient.respond(executionId, RespondBody.reject(reason)); } /** @@ -119,23 +157,20 @@ public void reject(String reason) { * @param data the response payload */ public void respond(Map data) { - httpApi.respond(executionId, data); + agentClient.respond(executionId, RespondBody.of(data)); } /** - * Send a message to a waiting agent. + * Send a message to a waiting agent (equivalent to approve with no comment). * - * @param message the message to send + * @param message ignored — kept for API compatibility */ public void send(String message) { - httpApi.respond(executionId, approveBody(null)); - } + agentClient.respond(executionId, RespondBody.approve()); - private static Map approveBody(String reason) { - Map body = new java.util.HashMap<>(); - body.put("approved", true); - if (reason != null && !reason.isEmpty()) body.put("reason", reason); - return body; + // placeholder to suppress unused-parameter warning + @SuppressWarnings("unused") + String ignored = message; } /** @@ -145,9 +180,8 @@ private static Map approveBody(String reason) { */ public boolean isWaiting() { try { - Map status = httpApi.getAgentStatus(executionId); - Object waiting = status.get("isWaiting"); - return Boolean.TRUE.equals(waiting); + AgentStatusResponse status = agentClient.getAgentStatus(executionId); + return status.isWaiting(); } catch (Exception e) { return false; } @@ -163,18 +197,19 @@ public boolean waitUntilWaiting(long timeoutMs) { long start = System.currentTimeMillis(); while (System.currentTimeMillis() - start < timeoutMs) { try { - Map status = httpApi.getAgentStatus(executionId); - Object waiting = status.get("isWaiting"); - if (Boolean.TRUE.equals(waiting)) return true; - String workflowStatus = (String) status.get("status"); - if (workflowStatus != null && isTerminalStatus(workflowStatus)) return false; + AgentStatusResponse status = agentClient.getAgentStatus(executionId); + if (status.isWaiting()) return true; + if (status.getStatus() != null && isTerminalStatus(status.getStatus())) return false; Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } catch (Exception e) { - try { Thread.sleep(1000); } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); return false; + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return false; } } } @@ -183,17 +218,14 @@ public boolean waitUntilWaiting(long timeoutMs) { private boolean isTerminalStatus(String status) { return "COMPLETED".equals(status) - || "FAILED".equals(status) - || "TERMINATED".equals(status) - || "TIMED_OUT".equals(status); + || "FAILED".equals(status) + || "TERMINATED".equals(status) + || "TIMED_OUT".equals(status); } @SuppressWarnings("unchecked") - private AgentResult buildResult(Map statusResponse, String workflowStatus) { - Object output = statusResponse.get("output"); - if (output == null) { - output = statusResponse.get("result"); - } + private AgentResult buildResult(AgentStatusResponse statusResponse, String workflowStatus) { + Object output = statusResponse.getOutput(); AgentStatus status; try { @@ -204,8 +236,7 @@ private AgentResult buildResult(Map statusResponse, String workf String error = null; if (status != AgentStatus.COMPLETED) { - error = (String) statusResponse.get("reasonForIncompletion"); - if (error == null) error = (String) statusResponse.get("error"); + error = statusResponse.getReasonForIncompletion(); } // Normalize output to a map @@ -220,47 +251,41 @@ private AgentResult buildResult(Map statusResponse, String workf // tokenUsed/promptTokens/completionTokens in its outputData, and every // tool-worker SIMPLE task in the workflow corresponds to one LLM tool // call. Walk the workflow tasks once and aggregate both. + // WorkflowClient is the standard Conductor client for /api/workflow/* — + // no need to go through AgentClient for this standard endpoint. TokenUsage tokenUsage = null; List> toolCalls = new ArrayList<>(); try { - Map workflow = httpApi.getWorkflow(executionId); - Object tasksRaw = workflow.get("tasks"); - if (tasksRaw instanceof List) { - int promptT = 0, completionT = 0, totalT = 0; - boolean sawTokens = false; - for (Object taskObj : (List) tasksRaw) { - if (!(taskObj instanceof Map)) continue; - Map task = (Map) taskObj; - String taskType = (String) task.get("taskType"); - Map outputData = (Map) task.get("outputData"); - - // LLM task — aggregate tokens - if ("LLM_CHAT_COMPLETE".equals(taskType) && outputData != null) { - promptT += toInt(outputData.get("promptTokens")); - completionT += toInt(outputData.get("completionTokens")); - totalT += toInt(outputData.get("tokenUsed")); - sawTokens = true; - continue; - } + Workflow workflow = workflowClient.getWorkflow(executionId, true); + List tasks = workflow != null ? workflow.getTasks() : List.of(); + int promptT = 0, completionT = 0, totalT = 0; + boolean sawTokens = false; + for (Task task : tasks) { + String taskType = task.getTaskType(); + Map outputData = task.getOutputData(); + + // LLM task — aggregate tokens + if ("LLM_CHAT_COMPLETE".equals(taskType) && outputData != null) { + promptT += toInt(outputData.get("promptTokens")); + completionT += toInt(outputData.get("completionTokens")); + totalT += toInt(outputData.get("tokenUsed")); + sawTokens = true; + continue; + } - // Tool worker task — capture name, input args (stripping - // internal Agentspan context), and output result. - // Server uses SIMPLE for workers, but the taskType field on - // the task instance is the worker name itself (e.g. "add"). - // We treat any non-system task whose name appears in the - // task definition as a worker tool call. - String refName = (String) task.get("referenceTaskName"); - if (refName != null - && refName.startsWith("call_") - && outputData != null) { - Map tc = new LinkedHashMap<>(); - tc.put("name", taskType); - Map inputData = (Map) task.get("inputData"); - if (inputData != null) { - Map cleaned = new LinkedHashMap<>(); - for (Map.Entry e : inputData.entrySet()) { - String k = e.getKey(); - if (k.startsWith("_") + // Tool worker task — capture name, input args (stripping + // internal Agentspan context), and output result. + // referenceTaskName starts with "call_" for LLM-dispatched tool calls. + String refName = task.getReferenceTaskName(); + if (refName != null && refName.startsWith("call_") && outputData != null) { + Map tc = new LinkedHashMap<>(); + tc.put("name", taskType); + Map inputData = task.getInputData(); + if (inputData != null) { + Map cleaned = new LinkedHashMap<>(); + for (Map.Entry e : inputData.entrySet()) { + String k = e.getKey(); + if (k.startsWith("_") || "method".equals(k) || "__agentspan_ctx__".equals(k) || "evaluatorType".equals(k) @@ -268,17 +293,16 @@ private AgentResult buildResult(Map statusResponse, String workf || "ctx".equals(k) || "workerTag".equals(k) || "agentConfig".equals(k)) continue; - cleaned.put(k, e.getValue()); - } - tc.put("args", cleaned); + cleaned.put(k, e.getValue()); } - tc.put("result", outputData.get("result")); - toolCalls.add(tc); + tc.put("args", cleaned); } + tc.put("result", outputData.get("result")); + toolCalls.add(tc); } - if (sawTokens) { - tokenUsage = new TokenUsage(promptT, completionT, totalT); - } + } + if (sawTokens) { + tokenUsage = new TokenUsage(promptT, completionT, totalT); } } catch (Exception e) { logger.debug("Could not extract tokens/toolCalls for {}: {}", executionId, e.getMessage()); diff --git a/sdk/java/src/main/java/ai/agentspan/model/AgentResult.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/AgentResult.java similarity index 88% rename from sdk/java/src/main/java/ai/agentspan/model/AgentResult.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/model/AgentResult.java index f0e23ef92..372b15d15 100644 --- a/sdk/java/src/main/java/ai/agentspan/model/AgentResult.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/AgentResult.java @@ -1,16 +1,17 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.model; - -import com.fasterxml.jackson.databind.ObjectMapper; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.internal.JsonMapper; +package org.conductoross.conductor.ai.model; import java.util.ArrayList; import java.util.List; import java.util.Map; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.internal.JsonMapper; + +import com.fasterxml.jackson.databind.ObjectMapper; + /** * The result of a completed agent execution. */ @@ -40,13 +41,33 @@ public AgentResult( this.error = error; } - public Object getOutput() { return output; } - public String getExecutionId() { return executionId; } - public AgentStatus getStatus() { return status; } - public List> getToolCalls() { return toolCalls; } - public List getEvents() { return events; } - public TokenUsage getTokenUsage() { return tokenUsage; } - public String getError() { return error; } + public Object getOutput() { + return output; + } + + public String getExecutionId() { + return executionId; + } + + public AgentStatus getStatus() { + return status; + } + + public List> getToolCalls() { + return toolCalls; + } + + public List getEvents() { + return events; + } + + public TokenUsage getTokenUsage() { + return tokenUsage; + } + + public String getError() { + return error; + } /** Returns true if the agent completed successfully. */ public boolean isSuccess() { diff --git a/sdk/java/src/main/java/ai/agentspan/model/AgentStream.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/AgentStream.java similarity index 79% rename from sdk/java/src/main/java/ai/agentspan/model/AgentStream.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/model/AgentStream.java index 72f65cfcf..f2023d380 100644 --- a/sdk/java/src/main/java/ai/agentspan/model/AgentStream.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/AgentStream.java @@ -1,14 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.model; - -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.enums.EventType; -import ai.agentspan.internal.HttpApi; -import ai.agentspan.internal.SseClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +package org.conductoross.conductor.ai.model; import java.util.ArrayList; import java.util.Iterator; @@ -16,6 +9,15 @@ import java.util.Map; import java.util.NoSuchElementException; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.enums.EventType; +import org.conductoross.conductor.ai.internal.AgentClient; +import org.conductoross.conductor.ai.internal.AgentStatusResponse; +import org.conductoross.conductor.ai.internal.RespondBody; +import org.conductoross.conductor.ai.internal.SseClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * A streaming view of an agent execution. * @@ -38,15 +40,15 @@ public class AgentStream implements Iterable, AutoCloseable { private final String executionId; private final SseClient sseClient; - private final HttpApi httpApi; + private final AgentClient agentClient; private final List capturedEvents = new ArrayList<>(); private AgentResult result; private boolean exhausted = false; - public AgentStream(String executionId, SseClient sseClient, HttpApi httpApi) { + public AgentStream(String executionId, SseClient sseClient, AgentClient agentClient) { this.executionId = executionId; this.sseClient = sseClient; - this.httpApi = httpApi; + this.agentClient = agentClient; } public String getExecutionId() { @@ -98,8 +100,8 @@ public AgentResult waitForResult(long timeoutMs, long pollIntervalMs) { long start = System.currentTimeMillis(); while (System.currentTimeMillis() - start < timeoutMs) { try { - Map status = httpApi.getAgentStatus(executionId); - String workflowStatus = (String) status.get("status"); + AgentStatusResponse status = agentClient.getAgentStatus(executionId); + String workflowStatus = status.getStatus(); if (workflowStatus != null && isTerminalStatus(workflowStatus)) { result = buildResultFromStatus(status, workflowStatus); return result; @@ -118,21 +120,19 @@ public AgentResult waitForResult(long timeoutMs, long pollIntervalMs) { } } } - throw new RuntimeException( - "Timed out after " + timeoutMs + "ms waiting for stream result: " + executionId); + throw new RuntimeException("Timed out after " + timeoutMs + "ms waiting for stream result: " + executionId); } private static boolean isTerminalStatus(String status) { return "COMPLETED".equals(status) - || "FAILED".equals(status) - || "TERMINATED".equals(status) - || "TIMED_OUT".equals(status); + || "FAILED".equals(status) + || "TERMINATED".equals(status) + || "TIMED_OUT".equals(status); } @SuppressWarnings("unchecked") - private AgentResult buildResultFromStatus(Map statusResponse, String workflowStatus) { - Object output = statusResponse.get("output"); - if (output == null) output = statusResponse.get("result"); + private AgentResult buildResultFromStatus(AgentStatusResponse statusResponse, String workflowStatus) { + Object output = statusResponse.getOutput(); AgentStatus status; try { @@ -143,8 +143,7 @@ private AgentResult buildResultFromStatus(Map statusResponse, St String error = null; if (status != AgentStatus.COMPLETED) { - error = (String) statusResponse.get("reasonForIncompletion"); - if (error == null) error = (String) statusResponse.get("error"); + error = statusResponse.getReasonForIncompletion(); } if (output == null) { @@ -154,9 +153,7 @@ private AgentResult buildResultFromStatus(Map statusResponse, St } return new AgentResult( - output, executionId, status, - new ArrayList<>(), new ArrayList<>(capturedEvents), - null, error); + output, executionId, status, new ArrayList<>(), new ArrayList<>(capturedEvents), null, error); } /** @@ -169,27 +166,23 @@ private AgentResult buildResultFromStatus(Map statusResponse, St *
  • Your sub-agent topology routes approvals to a HUMAN task at the top level.
  • * * - *

    Under {@link ai.agentspan.enums.Strategy#HANDOFF}, {@code SEQUENTIAL}, or + *

    Under {@link org.conductoross.conductor.ai.enums.Strategy#HANDOFF}, {@code SEQUENTIAL}, or * {@code PARALLEL} the HUMAN task usually lives in a sub-execution (the * sub-agent's own workflow). In that case this method POSTs to the wrong * execution id and the server returns HTTP 500 ("No pending HUMAN task found"): * use {@link #approve(AgentEvent)} with the {@code WAITING} event instead. */ public void approve() { - httpApi.respond(executionId, approveBody(null)); + agentClient.respond(executionId, RespondBody.approve()); } /** * Approve the pending HUMAN task associated with the given {@code WAITING} event. * - *

    Reads the owning execution id from {@link AgentEvent#getExecutionId()} — - * the sub-execution that emitted the event — and POSTs to it. Use this whenever - * the HUMAN task may live below the top level (handoff/sequential/parallel). - * * @param event the WAITING event whose pending HUMAN task should be approved */ public void approve(AgentEvent event) { - httpApi.respond(targetExecutionId(event), approveBody(null)); + agentClient.respond(targetExecutionId(event), RespondBody.approve()); } /** @@ -198,7 +191,7 @@ public void approve(AgentEvent event) { * @param reason optional rejection reason */ public void reject(String reason) { - httpApi.respond(executionId, rejectBody(reason)); + agentClient.respond(executionId, RespondBody.reject(reason)); } /** @@ -208,7 +201,7 @@ public void reject(String reason) { * @param reason optional rejection reason */ public void reject(AgentEvent event, String reason) { - httpApi.respond(targetExecutionId(event), rejectBody(reason)); + agentClient.respond(targetExecutionId(event), RespondBody.reject(reason)); } /** @@ -217,9 +210,7 @@ public void reject(AgentEvent event, String reason) { * @param message the message to send */ public void send(String message) { - java.util.Map body = new java.util.HashMap<>(); - body.put("message", message); - httpApi.respond(executionId, body); + agentClient.respond(executionId, RespondBody.of(java.util.Map.of("message", message))); } /** @@ -229,23 +220,7 @@ public void send(String message) { * @param message the message to send */ public void send(AgentEvent event, String message) { - java.util.Map body = new java.util.HashMap<>(); - body.put("message", message); - httpApi.respond(targetExecutionId(event), body); - } - - private static java.util.Map approveBody(String reason) { - java.util.Map body = new java.util.HashMap<>(); - body.put("approved", true); - if (reason != null && !reason.isEmpty()) body.put("reason", reason); - return body; - } - - private static java.util.Map rejectBody(String reason) { - java.util.Map body = new java.util.HashMap<>(); - body.put("approved", false); - if (reason != null && !reason.isEmpty()) body.put("reason", reason); - return body; + agentClient.respond(targetExecutionId(event), RespondBody.of(java.util.Map.of("message", message))); } private static String targetExecutionId(AgentEvent event) { diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/model/CompileResponse.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/CompileResponse.java new file mode 100644 index 000000000..69f9b0e60 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/CompileResponse.java @@ -0,0 +1,47 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.model; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response from {@code POST /api/agent/compile}. + * + *

    Returned by {@link org.conductoross.conductor.ai.AgentRuntime#plan(org.conductoross.conductor.ai.Agent)}. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class CompileResponse { + + @JsonProperty("workflowDef") + private Map workflowDef; + + @JsonProperty("requiredWorkers") + private List requiredWorkers; + + public CompileResponse() {} + + /** The compiled Conductor workflow definition. */ + public Map getWorkflowDef() { + return workflowDef != null ? workflowDef : Collections.emptyMap(); + } + + /** + * Task type names the SDK must register local workers for before the agent + * can make progress. The SDK handles this automatically inside + * {@link org.conductoross.conductor.ai.AgentRuntime#run}. + */ + public List getRequiredWorkers() { + return requiredWorkers != null ? requiredWorkers : Collections.emptyList(); + } + + @Override + public String toString() { + return "CompileResponse{requiredWorkers=" + getRequiredWorkers() + "}"; + } +} diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/model/ConversationMemory.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/ConversationMemory.java new file mode 100644 index 000000000..0d4ccc50d --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/ConversationMemory.java @@ -0,0 +1,86 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Conversation memory for stateful / multi-turn agents. + * + *

    Holds accumulated messages and an optional cap. Attached to an agent via + * {@code Agent.builder().memory(...)}; serialized to the server's {@code MemoryConfig} + * as {@code {messages, maxMessages}}. Mirrors the Python SDK's {@code ConversationMemory}. + * + *

    Each message is a map of the shape {@code {"role": "user"|"assistant"|"system", + * "message": ""}}. + * + *

    {@code
    + * ConversationMemory memory = new ConversationMemory(20)   // keep the last 20 messages
    + *     .addSystem("You are a concise assistant.")
    + *     .addUser("Hello");
    + *
    + * Agent agent = Agent.builder().name("chat").model("openai/gpt-4o-mini").memory(memory).build();
    + * }
    + */ +public class ConversationMemory { + + private final List> messages; + private final Integer maxMessages; + + /** Empty memory with no cap. */ + public ConversationMemory() { + this(null); + } + + /** + * Empty memory that retains at most {@code maxMessages} messages (oldest trimmed server-side). + * + * @param maxMessages maximum messages to retain, or {@code null} for unbounded + */ + public ConversationMemory(Integer maxMessages) { + this.messages = new ArrayList<>(); + this.maxMessages = maxMessages; + } + + /** Append a user message. Returns {@code this} for chaining. */ + public ConversationMemory addUser(String content) { + return add("user", content); + } + + /** Append an assistant message. Returns {@code this} for chaining. */ + public ConversationMemory addAssistant(String content) { + return add("assistant", content); + } + + /** Append a system message. Returns {@code this} for chaining. */ + public ConversationMemory addSystem(String content) { + return add("system", content); + } + + private ConversationMemory add(String role, String content) { + Map m = new LinkedHashMap<>(); + m.put("role", role); + m.put("message", content); + messages.add(m); + return this; + } + + /** The accumulated messages (mutable backing list). */ + public List> getMessages() { + return messages; + } + + /** Maximum messages to retain, or {@code null} for unbounded. */ + public Integer getMaxMessages() { + return maxMessages; + } + + /** Remove all messages. */ + public void clear() { + messages.clear(); + } +} diff --git a/sdk/java/src/main/java/ai/agentspan/model/CredentialFile.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/CredentialFile.java similarity index 84% rename from sdk/java/src/main/java/ai/agentspan/model/CredentialFile.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/model/CredentialFile.java index 708f5dc26..35f4a267b 100644 --- a/sdk/java/src/main/java/ai/agentspan/model/CredentialFile.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/CredentialFile.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.model; +package org.conductoross.conductor.ai.model; /** * A credential that should be written to a file in the subprocess HOME directory. @@ -27,13 +27,19 @@ public CredentialFile(String envVar, String relativePath, String content) { } /** Environment variable name that will point to the resolved file path (e.g. {@code "KUBECONFIG"}). */ - public String getEnvVar() { return envVar; } + public String getEnvVar() { + return envVar; + } /** Path relative to the subprocess temp HOME directory (e.g. {@code ".kube/config"}). */ - public String getRelativePath() { return relativePath; } + public String getRelativePath() { + return relativePath; + } /** File content (set by fetcher after resolving). {@code null} means not yet resolved. */ - public String getContent() { return content; } + public String getContent() { + return content; + } public CredentialFile withContent(String content) { return new CredentialFile(envVar, relativePath, content); diff --git a/sdk/java/src/main/java/ai/agentspan/model/DeploymentInfo.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/DeploymentInfo.java similarity index 83% rename from sdk/java/src/main/java/ai/agentspan/model/DeploymentInfo.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/model/DeploymentInfo.java index 1d0ce1794..9c57c8ee2 100644 --- a/sdk/java/src/main/java/ai/agentspan/model/DeploymentInfo.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/DeploymentInfo.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.model; +package org.conductoross.conductor.ai.model; /** * Result of deploying an agent to the server. @@ -23,10 +23,14 @@ public DeploymentInfo(String registeredName, String agentName) { } /** The name under which this agent is registered on the server. */ - public String getRegisteredName() { return registeredName; } + public String getRegisteredName() { + return registeredName; + } /** The original agent name. */ - public String getAgentName() { return agentName; } + public String getAgentName() { + return agentName; + } @Override public String toString() { diff --git a/sdk/java/src/main/java/ai/agentspan/model/GuardrailDef.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/GuardrailDef.java similarity index 51% rename from sdk/java/src/main/java/ai/agentspan/model/GuardrailDef.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/model/GuardrailDef.java index 631a7362a..68d0fd57b 100644 --- a/sdk/java/src/main/java/ai/agentspan/model/GuardrailDef.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/GuardrailDef.java @@ -1,18 +1,18 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.model; - -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; +package org.conductoross.conductor.ai.model; import java.util.Map; import java.util.function.Function; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; + /** * Runtime model for a guardrail definition. * - *

    This is the runtime counterpart to the {@link ai.agentspan.annotations.GuardrailDef} + *

    This is the runtime counterpart to the {@link org.conductoross.conductor.ai.annotations.GuardrailDef} * annotation. Use {@link Builder} to create instances. */ public class GuardrailDef { @@ -34,13 +34,33 @@ private GuardrailDef(Builder builder) { this.config = builder.config; } - public String getName() { return name; } - public Position getPosition() { return position; } - public OnFail getOnFail() { return onFail; } - public int getMaxRetries() { return maxRetries; } - public Function getFunc() { return func; } - public String getGuardrailType() { return guardrailType; } - public Map getConfig() { return config; } + public String getName() { + return name; + } + + public Position getPosition() { + return position; + } + + public OnFail getOnFail() { + return onFail; + } + + public int getMaxRetries() { + return maxRetries; + } + + public Function getFunc() { + return func; + } + + public String getGuardrailType() { + return guardrailType; + } + + public Map getConfig() { + return config; + } public static Builder builder() { return new Builder(); @@ -55,13 +75,40 @@ public static class Builder { private String guardrailType = "custom"; private Map config; - public Builder name(String name) { this.name = name; return this; } - public Builder position(Position position) { this.position = position; return this; } - public Builder onFail(OnFail onFail) { this.onFail = onFail; return this; } - public Builder maxRetries(int maxRetries) { this.maxRetries = maxRetries; return this; } - public Builder func(Function func) { this.func = func; return this; } - public Builder guardrailType(String guardrailType) { this.guardrailType = guardrailType; return this; } - public Builder config(Map config) { this.config = config; return this; } + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder position(Position position) { + this.position = position; + return this; + } + + public Builder onFail(OnFail onFail) { + this.onFail = onFail; + return this; + } + + public Builder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + public Builder func(Function func) { + this.func = func; + return this; + } + + public Builder guardrailType(String guardrailType) { + this.guardrailType = guardrailType; + return this; + } + + public Builder config(Map config) { + this.config = config; + return this; + } public GuardrailDef build() { if (name == null || name.isEmpty()) { diff --git a/sdk/java/src/main/java/ai/agentspan/model/GuardrailResult.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/GuardrailResult.java similarity index 96% rename from sdk/java/src/main/java/ai/agentspan/model/GuardrailResult.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/model/GuardrailResult.java index b27c15658..d1d3e9f71 100644 --- a/sdk/java/src/main/java/ai/agentspan/model/GuardrailResult.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/GuardrailResult.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.model; +package org.conductoross.conductor.ai.model; /** * Result returned from a guardrail function. diff --git a/sdk/java/src/main/java/ai/agentspan/model/PrefillToolCall.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/PrefillToolCall.java similarity index 83% rename from sdk/java/src/main/java/ai/agentspan/model/PrefillToolCall.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/model/PrefillToolCall.java index c21879ea9..b799e8ac5 100644 --- a/sdk/java/src/main/java/ai/agentspan/model/PrefillToolCall.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/PrefillToolCall.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See LICENSE file in the project root for details. */ -package ai.agentspan.model; +package org.conductoross.conductor.ai.model; import java.util.Collections; import java.util.Map; @@ -23,8 +23,13 @@ public PrefillToolCall(String toolName, Map arguments) { this.arguments = arguments != null ? arguments : Collections.emptyMap(); } - public String getToolName() { return toolName; } - public Map getArguments() { return arguments; } + public String getToolName() { + return toolName; + } + + public Map getArguments() { + return arguments; + } /** * Create a PrefillToolCall from a tool name and arguments. diff --git a/sdk/java/src/main/java/ai/agentspan/model/PromptTemplate.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/PromptTemplate.java similarity index 85% rename from sdk/java/src/main/java/ai/agentspan/model/PromptTemplate.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/model/PromptTemplate.java index 9c3aee04e..3e67ad88b 100644 --- a/sdk/java/src/main/java/ai/agentspan/model/PromptTemplate.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/PromptTemplate.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.model; +package org.conductoross.conductor.ai.model; import java.util.Map; @@ -49,7 +49,15 @@ public PromptTemplate(String name, Map variables, Integer versio this.version = version; } - public String getName() { return name; } - public Map getVariables() { return variables; } - public Integer getVersion() { return version; } + public String getName() { + return name; + } + + public Map getVariables() { + return variables; + } + + public Integer getVersion() { + return version; + } } diff --git a/sdk/java/src/main/java/ai/agentspan/model/TokenUsage.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/TokenUsage.java similarity index 88% rename from sdk/java/src/main/java/ai/agentspan/model/TokenUsage.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/model/TokenUsage.java index c81a2da0d..f63912899 100644 --- a/sdk/java/src/main/java/ai/agentspan/model/TokenUsage.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/TokenUsage.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.model; +package org.conductoross.conductor.ai.model; /** * Aggregated token usage across all LLM calls in an agent execution. @@ -31,7 +31,7 @@ public int getTotalTokens() { @Override public String toString() { - return "TokenUsage{prompt=" + promptTokens + ", completion=" + completionTokens - + ", total=" + totalTokens + "}"; + return "TokenUsage{prompt=" + promptTokens + ", completion=" + completionTokens + ", total=" + totalTokens + + "}"; } } diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/model/ToolContext.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/ToolContext.java new file mode 100644 index 000000000..765bab5a3 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/ToolContext.java @@ -0,0 +1,114 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.model; + +import java.util.HashMap; +import java.util.Map; + +import org.conductoross.conductor.ai.exceptions.CredentialNotFoundException; + +/** + * Context passed to tool functions during execution. + * + *

    Declare a {@code ToolContext} parameter on a {@code @Tool} method and the worker + * framework injects a per-call instance: + * + *

    {@code
    + * @Tool(credentials = {"GITHUB_TOKEN"})
    + * public String fetchIssue(String repo, ToolContext ctx) {
    + *     String token = ctx.getCredential("GITHUB_TOKEN");
    + *     ...
    + * }
    + * }
    + * + *

    {@link #getState()} provides a mutable dictionary that persists across all tool + * calls within the same agent execution. Tools can read and write to it to share + * data without relying on the LLM to relay state (mirrors Python SDK's + * {@code ToolContext.state}). + * + *

    {@link #getCredential(String)} returns a secret declared in + * {@code @Tool(credentials = {...})} and resolved by the runtime for this call. The + * credential map is an immutable per-call snapshot, so it is safe to read from threads + * the tool spawns — unlike a thread-local, the values remain valid for the lifetime of + * this context object. See {@code docs/design/secret-injection-contract.md} for the + * cross-SDK contract; Java's per-call context mirrors .NET's {@code IToolContext} and + * Python's contextvars accessor. + */ +public class ToolContext { + private final String sessionId; + private final String executionId; + private final String taskId; + private final Map state; + private final Map credentials; + + public ToolContext(String sessionId, String executionId, String taskId) { + this(sessionId, executionId, taskId, new HashMap<>()); + } + + public ToolContext(String sessionId, String executionId, String taskId, Map initialState) { + this(sessionId, executionId, taskId, initialState, null); + } + + public ToolContext( + String sessionId, + String executionId, + String taskId, + Map initialState, + Map credentials) { + this.sessionId = sessionId; + this.executionId = executionId; + this.taskId = taskId; + this.state = initialState != null ? new HashMap<>(initialState) : new HashMap<>(); + // Immutable snapshot: safe to publish to threads the tool spawns. + this.credentials = (credentials == null || credentials.isEmpty()) ? Map.of() : Map.copyOf(credentials); + } + + public String getSessionId() { + return sessionId; + } + + public String getExecutionId() { + return executionId; + } + + public String getTaskId() { + return taskId; + } + + /** + * Shared state dictionary persisted across tool calls within the same agent execution. + * Mutate this map to pass data to subsequent tool calls. + */ + public Map getState() { + return state; + } + + /** + * Read a secret declared in {@code @Tool(credentials = {...})} and resolved for this call. + * + * @param name the declared credential name + * @return the plaintext value + * @throws CredentialNotFoundException if the name was not declared / not resolved for this call + */ + public String getCredential(String name) { + String value = credentials.get(name); + if (value == null) { + throw new CredentialNotFoundException(name); + } + return value; + } + + /** + * Read a resolved secret, or {@code null} if it was not declared / not resolved. + * Use when you want to fall back gracefully instead of failing the tool. + */ + public String getCredentialOrNull(String name) { + return credentials.get(name); + } + + /** An immutable view of all secrets resolved for this call. */ + public Map getCredentials() { + return credentials; + } +} diff --git a/sdk/java/src/main/java/ai/agentspan/model/ToolDef.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/ToolDef.java similarity index 55% rename from sdk/java/src/main/java/ai/agentspan/model/ToolDef.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/model/ToolDef.java index 7bcf71652..31516d0fb 100644 --- a/sdk/java/src/main/java/ai/agentspan/model/ToolDef.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/model/ToolDef.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.model; - -import ai.agentspan.Agent; +package org.conductoross.conductor.ai.model; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Function; +import org.conductoross.conductor.ai.Agent; + /** * Definition of a tool that can be used by an agent. * @@ -61,23 +61,73 @@ private ToolDef(Builder builder) { this.stateful = builder.stateful; } - public String getName() { return name; } - public String getDescription() { return description; } - public Map getInputSchema() { return inputSchema; } - public Map getOutputSchema() { return outputSchema; } - public Function, Object> getFunc() { return func; } - public boolean isApprovalRequired() { return approvalRequired; } - public int getTimeoutSeconds() { return timeoutSeconds; } - public int getRetryCount() { return retryCount; } - public int getRetryDelaySeconds() { return retryDelaySeconds; } - public String getRetryPolicy() { return retryPolicy; } - public String getToolType() { return toolType; } - public Map getConfig() { return config; } - public List getCredentials() { return credentials; } - public List getGuardrails() { return guardrails; } - public int getMaxCalls() { return maxCalls; } - public Agent getAgentRef() { return agentRef; } - public boolean isStateful() { return stateful; } + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Map getInputSchema() { + return inputSchema; + } + + public Map getOutputSchema() { + return outputSchema; + } + + public Function, Object> getFunc() { + return func; + } + + public boolean isApprovalRequired() { + return approvalRequired; + } + + public int getTimeoutSeconds() { + return timeoutSeconds; + } + + public int getRetryCount() { + return retryCount; + } + + public int getRetryDelaySeconds() { + return retryDelaySeconds; + } + + public String getRetryPolicy() { + return retryPolicy; + } + + public String getToolType() { + return toolType; + } + + public Map getConfig() { + return config; + } + + public List getCredentials() { + return credentials; + } + + public List getGuardrails() { + return guardrails; + } + + public int getMaxCalls() { + return maxCalls; + } + + public Agent getAgentRef() { + return agentRef; + } + + public boolean isStateful() { + return stateful; + } public static Builder builder() { return new Builder(); @@ -102,27 +152,93 @@ public static class Builder { private Agent agentRef; private boolean stateful = false; - public Builder name(String name) { this.name = name; return this; } - public Builder description(String description) { this.description = description; return this; } - public Builder inputSchema(Map inputSchema) { this.inputSchema = inputSchema; return this; } - public Builder outputSchema(Map outputSchema) { this.outputSchema = outputSchema; return this; } - public Builder func(Function, Object> func) { this.func = func; return this; } - public Builder approvalRequired(boolean approvalRequired) { this.approvalRequired = approvalRequired; return this; } - public Builder timeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; return this; } - public Builder retryCount(int retryCount) { this.retryCount = retryCount; return this; } - public Builder retryDelaySeconds(int retryDelaySeconds) { this.retryDelaySeconds = retryDelaySeconds; return this; } - public Builder retryPolicy(String retryPolicy) { this.retryPolicy = retryPolicy; return this; } - public Builder toolType(String toolType) { this.toolType = toolType; return this; } - public Builder config(Map config) { this.config = config; return this; } - public Builder credentials(List credentials) { this.credentials = credentials; return this; } - public Builder guardrails(List guardrails) { this.guardrails = guardrails; return this; } - public Builder maxCalls(int maxCalls) { this.maxCalls = maxCalls; return this; } - public Builder agentRef(Agent agentRef) { this.agentRef = agentRef; return this; } + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder inputSchema(Map inputSchema) { + this.inputSchema = inputSchema; + return this; + } + + public Builder outputSchema(Map outputSchema) { + this.outputSchema = outputSchema; + return this; + } + + public Builder func(Function, Object> func) { + this.func = func; + return this; + } + + public Builder approvalRequired(boolean approvalRequired) { + this.approvalRequired = approvalRequired; + return this; + } + + public Builder timeoutSeconds(int timeoutSeconds) { + this.timeoutSeconds = timeoutSeconds; + return this; + } + + public Builder retryCount(int retryCount) { + this.retryCount = retryCount; + return this; + } + + public Builder retryDelaySeconds(int retryDelaySeconds) { + this.retryDelaySeconds = retryDelaySeconds; + return this; + } + + public Builder retryPolicy(String retryPolicy) { + this.retryPolicy = retryPolicy; + return this; + } + + public Builder toolType(String toolType) { + this.toolType = toolType; + return this; + } + + public Builder config(Map config) { + this.config = config; + return this; + } + + public Builder credentials(List credentials) { + this.credentials = credentials; + return this; + } + + public Builder guardrails(List guardrails) { + this.guardrails = guardrails; + return this; + } + + public Builder maxCalls(int maxCalls) { + this.maxCalls = maxCalls; + return this; + } + + public Builder agentRef(Agent agentRef) { + this.agentRef = agentRef; + return this; + } /** * Mark this tool as stateful so the runtime routes its tasks to a * per-execution worker domain. Mirrors Python {@code @tool(stateful=True)}. */ - public Builder stateful(boolean stateful) { this.stateful = stateful; return this; } + public Builder stateful(boolean stateful) { + this.stateful = stateful; + return this; + } public ToolDef build() { if (name == null || name.isEmpty()) { diff --git a/sdk/java/src/main/java/ai/agentspan/openai/GPTAssistantAgent.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/openai/GPTAssistantAgent.java similarity index 70% rename from sdk/java/src/main/java/ai/agentspan/openai/GPTAssistantAgent.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/openai/GPTAssistantAgent.java index e3b8e26f8..13e8c1e36 100644 --- a/sdk/java/src/main/java/ai/agentspan/openai/GPTAssistantAgent.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/openai/GPTAssistantAgent.java @@ -1,10 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.openai; - -import ai.agentspan.Agent; -import ai.agentspan.model.ToolDef; +package org.conductoross.conductor.ai.openai; import java.net.URI; import java.net.http.HttpClient; @@ -16,6 +13,9 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.model.ToolDef; + /** * An agent backed by the OpenAI Assistants API. * @@ -59,20 +59,40 @@ private Builder(String name) { this.name = name; } - public Builder assistantId(String assistantId) { this.assistantId = assistantId; return this; } - public Builder model(String model) { this.model = model; return this; } - public Builder instructions(String instructions) { this.instructions = instructions; return this; } - public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; } + public Builder assistantId(String assistantId) { + this.assistantId = assistantId; + return this; + } + + public Builder model(String model) { + this.model = model; + return this; + } + + public Builder instructions(String instructions) { + this.instructions = instructions; + return this; + } + + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } public Builder openaiTool(String type) { this.openaiTools.add(Map.of("type", type)); return this; } + public Builder openaiTool(Map tool) { this.openaiTools.add(new LinkedHashMap<>(tool)); return this; } - public Builder tool(ToolDef tool) { this.extraTools.add(tool); return this; } + + public Builder tool(ToolDef tool) { + this.extraTools.add(tool); + return this; + } public Agent build() { String resolvedModel = model != null && !model.contains("/") ? "openai/" + model : model; @@ -94,41 +114,50 @@ public Agent build() { final AtomicReference assistantIdRef = new AtomicReference<>(capturedAssistantId); ToolDef callTool = ToolDef.builder() - .name(agentName + "_assistant_call") - .description("Send a message to the OpenAI Assistant and get a response.") - .inputSchema(Map.of( - "type", "object", - "properties", Map.of( - "message", Map.of("type", "string", "description", "The message to send") - ), - "required", List.of("message") - )) - .func(input -> { - String message = input.get("message") instanceof String - ? (String) input.get("message") : String.valueOf(input); - return runAssistant(message, assistantIdRef, capturedModel, - capturedInstructions, capturedTools, capturedApiKey); - }) - .build(); + .name(agentName + "_assistant_call") + .description("Send a message to the OpenAI Assistant and get a response.") + .inputSchema(Map.of( + "type", "object", + "properties", + Map.of("message", Map.of("type", "string", "description", "The message to send")), + "required", List.of("message"))) + .func(input -> { + String message = input.get("message") instanceof String + ? (String) input.get("message") + : String.valueOf(input); + return runAssistant( + message, + assistantIdRef, + capturedModel, + capturedInstructions, + capturedTools, + capturedApiKey); + }) + .build(); List allTools = new ArrayList<>(); allTools.add(callTool); allTools.addAll(extraTools); return Agent.builder() - .name(name) - .model(resolvedModel) - .instructions(resolvedInstructions) - .tools(allTools) - .metadata(metadata) - .maxTurns(1) - .build(); + .name(name) + .model(resolvedModel) + .instructions(resolvedInstructions) + .tools(allTools) + .metadata(metadata) + .maxTurns(1) + .build(); } } @SuppressWarnings("unchecked") - private static String runAssistant(String message, AtomicReference assistantIdRef, - String model, String instructions, List> openaiTools, String apiKey) { + private static String runAssistant( + String message, + AtomicReference assistantIdRef, + String model, + String instructions, + List> openaiTools, + String apiKey) { try { String key = apiKey != null ? apiKey : System.getenv("OPENAI_API_KEY"); if (key == null || key.isEmpty()) { @@ -205,30 +234,29 @@ private static String runAssistant(String message, AtomicReference assis } @SuppressWarnings("unchecked") - private static Map apiPost(HttpClient client, String apiKey, - String path, Object body) throws Exception { - String json = ai.agentspan.internal.JsonMapper.toJson(body); + private static Map apiPost(HttpClient client, String apiKey, String path, Object body) + throws Exception { + String json = org.conductoross.conductor.ai.internal.JsonMapper.toJson(body); HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("https://api.openai.com/v1" + path)) - .header("Authorization", "Bearer " + apiKey) - .header("Content-Type", "application/json") - .header("OpenAI-Beta", "assistants=v2") - .POST(HttpRequest.BodyPublishers.ofString(json)) - .build(); + .uri(URI.create("https://api.openai.com/v1" + path)) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .header("OpenAI-Beta", "assistants=v2") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); HttpResponse resp = client.send(request, HttpResponse.BodyHandlers.ofString()); - return ai.agentspan.internal.JsonMapper.fromJson(resp.body(), Map.class); + return org.conductoross.conductor.ai.internal.JsonMapper.fromJson(resp.body(), Map.class); } @SuppressWarnings("unchecked") - private static Map apiGet(HttpClient client, String apiKey, - String path) throws Exception { + private static Map apiGet(HttpClient client, String apiKey, String path) throws Exception { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("https://api.openai.com/v1" + path)) - .header("Authorization", "Bearer " + apiKey) - .header("OpenAI-Beta", "assistants=v2") - .GET() - .build(); + .uri(URI.create("https://api.openai.com/v1" + path)) + .header("Authorization", "Bearer " + apiKey) + .header("OpenAI-Beta", "assistants=v2") + .GET() + .build(); HttpResponse resp = client.send(request, HttpResponse.BodyHandlers.ofString()); - return ai.agentspan.internal.JsonMapper.fromJson(resp.body(), Map.class); + return org.conductoross.conductor.ai.internal.JsonMapper.fromJson(resp.body(), Map.class); } } diff --git a/sdk/java/src/main/java/ai/agentspan/plans/Action.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Action.java similarity index 96% rename from sdk/java/src/main/java/ai/agentspan/plans/Action.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Action.java index 9614bb7b9..e6c1a0a3b 100644 --- a/sdk/java/src/main/java/ai/agentspan/plans/Action.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Action.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.plans; +package org.conductoross.conductor.ai.plans; import java.util.LinkedHashMap; import java.util.Map; diff --git a/sdk/java/src/main/java/ai/agentspan/plans/Context.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Context.java similarity index 99% rename from sdk/java/src/main/java/ai/agentspan/plans/Context.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Context.java index 321f14a09..e3dd91a89 100644 --- a/sdk/java/src/main/java/ai/agentspan/plans/Context.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Context.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.plans; +package org.conductoross.conductor.ai.plans; import java.util.LinkedHashMap; import java.util.Map; diff --git a/sdk/java/src/main/java/ai/agentspan/plans/Generate.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Generate.java similarity index 98% rename from sdk/java/src/main/java/ai/agentspan/plans/Generate.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Generate.java index a1b059f92..f2f860e91 100644 --- a/sdk/java/src/main/java/ai/agentspan/plans/Generate.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Generate.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.plans; +package org.conductoross.conductor.ai.plans; import java.util.LinkedHashMap; import java.util.Map; diff --git a/sdk/java/src/main/java/ai/agentspan/plans/Op.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Op.java similarity index 91% rename from sdk/java/src/main/java/ai/agentspan/plans/Op.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Op.java index 88028cbae..b165cb217 100644 --- a/sdk/java/src/main/java/ai/agentspan/plans/Op.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Op.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.plans; +package org.conductoross.conductor.ai.plans; import java.util.LinkedHashMap; import java.util.Map; @@ -21,8 +21,7 @@ public final class Op { private Op(Builder b) { if ((b.args == null) == (b.generate == null)) { - throw new IllegalArgumentException( - "Op('" + b.tool + "'): exactly one of args or generate must be set"); + throw new IllegalArgumentException("Op('" + b.tool + "'): exactly one of args or generate must be set"); } this.tool = b.tool; this.args = b.args; diff --git a/sdk/java/src/main/java/ai/agentspan/plans/Plan.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Plan.java similarity index 80% rename from sdk/java/src/main/java/ai/agentspan/plans/Plan.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Plan.java index fe33b642e..0637bbc2d 100644 --- a/sdk/java/src/main/java/ai/agentspan/plans/Plan.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Plan.java @@ -1,13 +1,18 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.plans; +package org.conductoross.conductor.ai.plans; +import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + /** * A compiled plan ready for {@code Strategy.PLAN_EXECUTE} execution. * @@ -94,4 +99,16 @@ public Plan build() { return new Plan(this); } } + + /** + * Jackson serializer that calls {@link #toJson()} so a {@code Plan}-typed field + * in {@code AgentRequest} writes the correct wire format without the caller + * pre-converting to a {@code Map}. + */ + public static final class AsJson extends JsonSerializer { + @Override + public void serialize(Plan plan, JsonGenerator gen, SerializerProvider provider) throws IOException { + provider.defaultSerializeValue(plan.toJson(), gen); + } + } } diff --git a/sdk/java/src/main/java/ai/agentspan/plans/PlanValues.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/PlanValues.java similarity index 97% rename from sdk/java/src/main/java/ai/agentspan/plans/PlanValues.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/plans/PlanValues.java index e4c1ed13e..d63c05b6a 100644 --- a/sdk/java/src/main/java/ai/agentspan/plans/PlanValues.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/PlanValues.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.plans; +package org.conductoross.conductor.ai.plans; import java.util.ArrayList; import java.util.LinkedHashMap; diff --git a/sdk/java/src/main/java/ai/agentspan/plans/Ref.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Ref.java similarity index 97% rename from sdk/java/src/main/java/ai/agentspan/plans/Ref.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Ref.java index f3d0c9e38..42289bc30 100644 --- a/sdk/java/src/main/java/ai/agentspan/plans/Ref.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Ref.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.plans; +package org.conductoross.conductor.ai.plans; import java.util.Map; import java.util.Objects; diff --git a/sdk/java/src/main/java/ai/agentspan/plans/Step.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Step.java similarity index 98% rename from sdk/java/src/main/java/ai/agentspan/plans/Step.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Step.java index 929311a15..9e812b191 100644 --- a/sdk/java/src/main/java/ai/agentspan/plans/Step.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Step.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.plans; +package org.conductoross.conductor.ai.plans; import java.util.ArrayList; import java.util.LinkedHashMap; diff --git a/sdk/java/src/main/java/ai/agentspan/plans/Validation.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Validation.java similarity index 97% rename from sdk/java/src/main/java/ai/agentspan/plans/Validation.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Validation.java index 35e934f03..e90910ea5 100644 --- a/sdk/java/src/main/java/ai/agentspan/plans/Validation.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/plans/Validation.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.plans; +package org.conductoross.conductor.ai.plans; import java.util.LinkedHashMap; import java.util.Map; diff --git a/sdk/java/src/main/java/ai/agentspan/schedule/Schedule.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/Schedule.java similarity index 55% rename from sdk/java/src/main/java/ai/agentspan/schedule/Schedule.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/Schedule.java index 474c918ae..1088cfe68 100644 --- a/sdk/java/src/main/java/ai/agentspan/schedule/Schedule.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/Schedule.java @@ -1,7 +1,7 @@ // Copyright (c) 2026 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.schedule; +package org.conductoross.conductor.ai.schedule; import java.util.Collections; import java.util.LinkedHashMap; @@ -50,17 +50,45 @@ private Schedule(Builder b) { this.description = b.description; } - public static Builder builder() { return new Builder(); } + public static Builder builder() { + return new Builder(); + } + + public String getName() { + return name; + } + + public String getCron() { + return cron; + } - public String getName() { return name; } - public String getCron() { return cron; } - public String getTimezone() { return timezone; } - public Map getInput() { return input; } - public boolean isCatchup() { return catchup; } - public boolean isPaused() { return paused; } - public Long getStartAt() { return startAt; } - public Long getEndAt() { return endAt; } - public String getDescription() { return description; } + public String getTimezone() { + return timezone; + } + + public Map getInput() { + return input; + } + + public boolean isCatchup() { + return catchup; + } + + public boolean isPaused() { + return paused; + } + + public Long getStartAt() { + return startAt; + } + + public Long getEndAt() { + return endAt; + } + + public String getDescription() { + return description; + } public static final class Builder { private String name; @@ -73,15 +101,53 @@ public static final class Builder { private Long endAt; private String description; - public Builder name(String v) { this.name = v; return this; } - public Builder cron(String v) { this.cron = v; return this; } - public Builder timezone(String v) { this.timezone = v; return this; } - public Builder input(Map v) { this.input = v; return this; } - public Builder catchup(boolean v) { this.catchup = v; return this; } - public Builder paused(boolean v) { this.paused = v; return this; } - public Builder startAt(Long v) { this.startAt = v; return this; } - public Builder endAt(Long v) { this.endAt = v; return this; } - public Builder description(String v) { this.description = v; return this; } - public Schedule build() { return new Schedule(this); } + public Builder name(String v) { + this.name = v; + return this; + } + + public Builder cron(String v) { + this.cron = v; + return this; + } + + public Builder timezone(String v) { + this.timezone = v; + return this; + } + + public Builder input(Map v) { + this.input = v; + return this; + } + + public Builder catchup(boolean v) { + this.catchup = v; + return this; + } + + public Builder paused(boolean v) { + this.paused = v; + return this; + } + + public Builder startAt(Long v) { + this.startAt = v; + return this; + } + + public Builder endAt(Long v) { + this.endAt = v; + return this; + } + + public Builder description(String v) { + this.description = v; + return this; + } + + public Schedule build() { + return new Schedule(this); + } } } diff --git a/sdk/java/src/main/java/ai/agentspan/schedule/ScheduleException.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/ScheduleException.java similarity index 72% rename from sdk/java/src/main/java/ai/agentspan/schedule/ScheduleException.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/ScheduleException.java index 458ac0aea..576dc7996 100644 --- a/sdk/java/src/main/java/ai/agentspan/schedule/ScheduleException.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/ScheduleException.java @@ -1,7 +1,7 @@ // Copyright (c) 2026 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.schedule; +package org.conductoross.conductor.ai.schedule; /** Base class for schedule errors. */ public class ScheduleException extends RuntimeException { @@ -15,16 +15,22 @@ public ScheduleException(String message, Throwable cause) { /** Two schedules in the same agent share a name. */ public static class NameConflict extends ScheduleException { - public NameConflict(String message) { super(message); } + public NameConflict(String message) { + super(message); + } } /** No schedule matches the given name. */ public static class NotFound extends ScheduleException { - public NotFound(String message) { super(message); } + public NotFound(String message) { + super(message); + } } /** Server rejected the cron expression as malformed. */ public static class InvalidCron extends ScheduleException { - public InvalidCron(String message) { super(message); } + public InvalidCron(String message) { + super(message); + } } } diff --git a/sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/ScheduleInfo.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/ScheduleInfo.java new file mode 100644 index 000000000..86ec97053 --- /dev/null +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/ScheduleInfo.java @@ -0,0 +1,137 @@ +// Copyright (c) 2026 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.schedule; + +import java.util.Map; + +/** Server view of a schedule, returned by {@link Schedules#list(String)} / {@link Schedules#get(String)}. */ +public final class ScheduleInfo { + private final String name; + private final String shortName; + private final String agent; + private final String cron; + private final String timezone; + private final Map input; + private final boolean paused; + private final String pausedReason; + private final boolean catchup; + private final Long startAt; + private final Long endAt; + private final String description; + private final Long nextRun; + private final Long createTime; + private final Long updateTime; + private final String createdBy; + private final String updatedBy; + + public ScheduleInfo( + String name, + String shortName, + String agent, + String cron, + String timezone, + Map input, + boolean paused, + String pausedReason, + boolean catchup, + Long startAt, + Long endAt, + String description, + Long nextRun, + Long createTime, + Long updateTime, + String createdBy, + String updatedBy) { + this.name = name; + this.shortName = shortName; + this.agent = agent; + this.cron = cron; + this.timezone = timezone; + this.input = input; + this.paused = paused; + this.pausedReason = pausedReason; + this.catchup = catchup; + this.startAt = startAt; + this.endAt = endAt; + this.description = description; + this.nextRun = nextRun; + this.createTime = createTime; + this.updateTime = updateTime; + this.createdBy = createdBy; + this.updatedBy = updatedBy; + } + + public String getName() { + return name; + } + + public String getShortName() { + return shortName; + } + + public String getAgent() { + return agent; + } + + public String getCron() { + return cron; + } + + public String getTimezone() { + return timezone; + } + + public Map getInput() { + return input; + } + + public boolean isPaused() { + return paused; + } + + public String getPausedReason() { + return pausedReason; + } + + public boolean isCatchup() { + return catchup; + } + + public Long getStartAt() { + return startAt; + } + + public Long getEndAt() { + return endAt; + } + + public String getDescription() { + return description; + } + + public Long getNextRun() { + return nextRun; + } + + public Long getCreateTime() { + return createTime; + } + + public Long getUpdateTime() { + return updateTime; + } + + public String getCreatedBy() { + return createdBy; + } + + public String getUpdatedBy() { + return updatedBy; + } + + @Override + public String toString() { + return "ScheduleInfo{name=" + name + ", agent=" + agent + ", cron=" + cron + ", paused=" + paused + "}"; + } +} diff --git a/sdk/java/src/main/java/ai/agentspan/schedule/Schedules.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/Schedules.java similarity index 52% rename from sdk/java/src/main/java/ai/agentspan/schedule/Schedules.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/Schedules.java index b2e6b082b..0d9153f6a 100644 --- a/sdk/java/src/main/java/ai/agentspan/schedule/Schedules.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/schedule/Schedules.java @@ -1,15 +1,8 @@ // Copyright (c) 2026 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.schedule; - -import java.net.URI; -import java.net.URLEncoder; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.time.Duration; +package org.conductoross.conductor.ai.schedule; + import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -17,91 +10,137 @@ import java.util.List; import java.util.Map; import java.util.Set; -import ai.agentspan.AgentConfig; -import ai.agentspan.exceptions.AgentAPIException; -import ai.agentspan.internal.JsonMapper; + +import org.conductoross.conductor.ai.exceptions.AgentAPIException; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.netflix.conductor.client.exception.ConductorClientException; +import com.netflix.conductor.client.http.ConductorClient; +import com.netflix.conductor.client.http.ConductorClientRequest; +import com.netflix.conductor.client.http.ConductorClientRequest.Method; +import com.netflix.conductor.client.http.WorkflowClient; +import com.netflix.conductor.common.metadata.workflow.StartWorkflowRequest; /** * Lifecycle API for cron-based agent schedules. Obtained via {@code runtime.schedules()}. * + *

    All requests ride the shared native Conductor {@link ConductorClient}/ApiClient + * (same HTTP + token-auth backend as every other client) — the scheduler CRUD via + * {@link ConductorClientRequest}/{@link ConductorClient#execute} ({@code /api/scheduler/*}, + * for which the Conductor client ships no typed {@code SchedulerClient}), and + * {@code runNow} via the typed {@link WorkflowClient}. + * *

    Operations are keyed by the wire name (prefixed with * {@code agent-}) returned by {@link #list(String)}. Use {@link Schedule} to * construct the user-facing short name; the SDK prefixes it at deploy time. */ public class Schedules { - private final AgentConfig config; - private final HttpClient http; + private static final TypeReference> MAP_TYPE = new TypeReference>() {}; + private static final TypeReference>> LIST_MAP_TYPE = + new TypeReference>>() {}; + private static final TypeReference> LIST_LONG_TYPE = new TypeReference>() {}; - public Schedules(AgentConfig config, HttpClient http) { - this.config = config; - this.http = http; + private final ConductorClient client; + /** Shared native Conductor client for starting workflows (runNow). */ + private final WorkflowClient workflowClient; + + public Schedules(ConductorClient conductorClient) { + this.client = conductorClient; + this.workflowClient = new WorkflowClient(conductorClient); } // ── CRUD ──────────────────────────────────────────────────────────── public void save(Schedule schedule, String agentName) { Map body = toSaveRequest(schedule, agentName); - request("POST", "/api/scheduler/schedules", body); + execVoid(ConductorClientRequest.builder() + .method(Method.POST) + .path("/scheduler/schedules") + .body(body) + .build()); } - @SuppressWarnings("unchecked") public ScheduleInfo get(String wireName) { - Object resp = request("GET", "/api/scheduler/schedules/" + enc(wireName), null); - if (!(resp instanceof Map) || ((Map) resp).isEmpty() || ((Map) resp).get("name") == null) { + Map resp = exec( + ConductorClientRequest.builder() + .method(Method.GET) + .path("/scheduler/schedules/{name}") + .addPathParam("name", wireName) + .build(), + MAP_TYPE); + if (resp == null || resp.isEmpty() || resp.get("name") == null) { throw new ScheduleException.NotFound("Schedule '" + wireName + "' not found"); } - return fromWorkflowSchedule((Map) resp, null); + return fromWorkflowSchedule(resp, null); } - @SuppressWarnings("unchecked") public List list(String agentName) { - Object resp = request( - "GET", "/api/scheduler/schedules?workflowName=" + enc(agentName), null); - if (!(resp instanceof List)) return new ArrayList<>(); + List> resp = exec( + ConductorClientRequest.builder() + .method(Method.GET) + .path("/scheduler/schedules") + .addQueryParam("workflowName", agentName) + .build(), + LIST_MAP_TYPE); + if (resp == null) return new ArrayList<>(); List out = new ArrayList<>(); - for (Object item : (List) resp) { - if (item instanceof Map) { - out.add(fromWorkflowSchedule((Map) item, agentName)); - } + for (Map item : resp) { + if (item != null) out.add(fromWorkflowSchedule(item, agentName)); } return out; } - public void pause(String wireName) { pause(wireName, null); } + public void pause(String wireName) { + pause(wireName, null); + } public void pause(String wireName, String reason) { - String path = "/api/scheduler/schedules/" + enc(wireName) + "/pause"; - if (reason != null) path += "?reason=" + enc(reason); - request("PUT", path, null); + ConductorClientRequest.Builder b = ConductorClientRequest.builder() + .method(Method.PUT) + .path("/scheduler/schedules/{name}/pause") + .addPathParam("name", wireName); + if (reason != null) b.addQueryParam("reason", reason); + execVoid(b.build()); } public void resume(String wireName) { - request("PUT", "/api/scheduler/schedules/" + enc(wireName) + "/resume", null); + execVoid(ConductorClientRequest.builder() + .method(Method.PUT) + .path("/scheduler/schedules/{name}/resume") + .addPathParam("name", wireName) + .build()); } public void delete(String wireName) { - request("DELETE", "/api/scheduler/schedules/" + enc(wireName), null); + execVoid(ConductorClientRequest.builder() + .method(Method.DELETE) + .path("/scheduler/schedules/{name}") + .addPathParam("name", wireName) + .build()); } - @SuppressWarnings("unchecked") + /** + * Start the scheduled agent's workflow immediately via the official Conductor + * {@link WorkflowClient#startWorkflow} (returns the new workflowId). + */ public String runNow(ScheduleInfo info) { - Object resp = request("POST", "/api/workflow/" + enc(info.getAgent()), info.getInput()); - if (resp instanceof String) return (String) resp; - if (resp instanceof Map) return String.valueOf(((Map) resp).get("workflowId")); - return String.valueOf(resp); + StartWorkflowRequest req = new StartWorkflowRequest(); + req.setName(info.getAgent()); + if (info.getInput() != null) req.setInput(info.getInput()); + return workflowClient.startWorkflow(req); } - @SuppressWarnings("unchecked") public List previewNext(String cron, int n) { - String path = "/api/scheduler/nextFewSchedules?cronExpression=" + enc(cron) + "&limit=" + n; - Object resp = request("GET", path, null); - if (!(resp instanceof List)) return new ArrayList<>(); - List out = new ArrayList<>(); - for (Object o : (List) resp) { - if (o instanceof Number) out.add(((Number) o).longValue()); - } - return out; + List resp = exec( + ConductorClientRequest.builder() + .method(Method.GET) + .path("/scheduler/nextFewSchedules") + .addQueryParam("cronExpression", cron) + .addQueryParam("limit", Integer.valueOf(n)) + .build(), + LIST_LONG_TYPE); + return resp != null ? resp : new ArrayList<>(); } // ── Declarative reconcile ─────────────────────────────────────────── @@ -205,72 +244,32 @@ private static Long longOrNull(Object o) { return o instanceof Number ? ((Number) o).longValue() : null; } - private static String enc(String s) { - return URLEncoder.encode(s, StandardCharsets.UTF_8); + /** Execute a scheduler request returning a typed body via the native Conductor client. */ + private T exec(ConductorClientRequest req, TypeReference type) { + try { + return client.execute(req, type).getData(); + } catch (ConductorClientException e) { + throw mapException(e); + } } - /** - * Issues an HTTP request and returns the parsed JSON body as a Map, List, String, or null. - * Translates known status codes into typed schedule exceptions. - */ - private Object request(String method, String path, Object body) { + /** Execute a scheduler request that returns no body. */ + private void execVoid(ConductorClientRequest req) { try { - String url = config.getServerUrl() + path; - String jsonBody = body == null ? null : JsonMapper.toJson(body); - - HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofSeconds(30)) - .header("Content-Type", "application/json"); - addAuthHeaders(builder); - - switch (method) { - case "GET": builder.GET(); break; - case "DELETE": builder.DELETE(); break; - case "POST": - builder.POST(jsonBody == null - ? HttpRequest.BodyPublishers.noBody() - : HttpRequest.BodyPublishers.ofString(jsonBody)); - break; - case "PUT": - builder.PUT(jsonBody == null - ? HttpRequest.BodyPublishers.noBody() - : HttpRequest.BodyPublishers.ofString(jsonBody)); - break; - default: throw new IllegalArgumentException("Unsupported method: " + method); - } - - HttpResponse response = http.send(builder.build(), HttpResponse.BodyHandlers.ofString()); - int status = response.statusCode(); - String text = response.body() == null ? "" : response.body(); - - if (status >= 400) { - if (status == 404) throw new ScheduleException.NotFound(text); - if (status == 400 && text.toLowerCase().contains("cron")) { - throw new ScheduleException.InvalidCron(text); - } - throw new AgentAPIException(status, text); - } - - if (text.isEmpty()) return null; - String trimmed = text.trim(); - if (trimmed.startsWith("{")) return JsonMapper.fromJson(trimmed, Map.class); - if (trimmed.startsWith("[")) return JsonMapper.fromJson(trimmed, List.class); - // Bare workflow id or string - return trimmed.replace("\"", ""); - } catch (ScheduleException | AgentAPIException e) { - throw e; - } catch (Exception e) { - throw new ScheduleException("HTTP " + method + " " + path + " failed", e); + client.execute(req); + } catch (ConductorClientException e) { + throw mapException(e); } } - private void addAuthHeaders(HttpRequest.Builder builder) { - if (config.getAuthKey() != null && !config.getAuthKey().isEmpty()) { - builder.header("X-Auth-Key", config.getAuthKey()); - } - if (config.getAuthSecret() != null && !config.getAuthSecret().isEmpty()) { - builder.header("X-Auth-Secret", config.getAuthSecret()); + /** Map Conductor's exception to the scheduler's typed exceptions (preserves the contract). */ + private static RuntimeException mapException(ConductorClientException e) { + int status = e.getStatus(); + String msg = e.getMessage() != null ? e.getMessage() : ""; + if (status == 404) return new ScheduleException.NotFound(msg); + if (status == 400 && msg.toLowerCase().contains("cron")) { + return new ScheduleException.InvalidCron(msg); } + return new AgentAPIException(status, msg); } } diff --git a/sdk/java/src/main/java/ai/agentspan/skill/Skill.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/skill/Skill.java similarity index 85% rename from sdk/java/src/main/java/ai/agentspan/skill/Skill.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/skill/Skill.java index c818241d4..fc94be41e 100644 --- a/sdk/java/src/main/java/ai/agentspan/skill/Skill.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/skill/Skill.java @@ -1,9 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.skill; - -import ai.agentspan.Agent; +package org.conductoross.conductor.ai.skill; import java.io.IOException; import java.io.InputStream; @@ -27,6 +25,8 @@ import java.util.regex.Pattern; import java.util.stream.Stream; +import org.conductoross.conductor.ai.Agent; + /** * Load an Agent Skills directory as an Agentspan Agent. * @@ -46,14 +46,13 @@ public class Skill { private static final Pattern FRONTMATTER = Pattern.compile("^---\\s*\\n(.*?)\\n---\\s*\\n", Pattern.DOTALL); private static final Pattern NAME_FIELD = Pattern.compile("(?m)^name:\\s*(.+)$"); private static final Pattern CROSS_SKILL = - Pattern.compile("(?i)(?:invoke|use|call)\\s+(?:the\\s+)?([a-z][a-z0-9-]*)\\s+skill"); + Pattern.compile("(?i)(?:invoke|use|call)\\s+(?:the\\s+)?([a-z][a-z0-9-]*)\\s+skill"); private static final int SECTION_SPLIT_THRESHOLD = 50000; private static final Map INTERPRETERS = Map.of( - "python", "python3", - "bash", "bash", - "node", "node", - "ruby", "ruby" - ); + "python", "python3", + "bash", "bash", + "node", "node", + "ruby", "ruby"); private Skill() {} @@ -118,8 +117,7 @@ public static Agent skill( Path skillMdPath = path.resolve("SKILL.md"); if (!Files.exists(skillMdPath)) { - throw new SkillLoadError( - "Directory " + path + " is not a valid skill: SKILL.md not found"); + throw new SkillLoadError("Directory " + path + " is not a valid skill: SKILL.md not found"); } String skillMd; @@ -193,8 +191,7 @@ public static Map loadSkills(Path path, String model) { * @param agentModels per-skill, per-sub-agent overrides (skill dir name → agent name → model) * @return map of skill name to Agent */ - public static Map loadSkills(Path path, String model, - Map> agentModels) { + public static Map loadSkills(Path path, String model, Map> agentModels) { return loadSkills(path, model, agentModels, null); } @@ -208,21 +205,21 @@ public static Map loadSkills(Path path, String model, * @param searchPath additional directories for cross-skill reference resolution * @return map of skill name to Agent */ - public static Map loadSkills(Path path, String model, - Map> agentModels, List searchPath) { + public static Map loadSkills( + Path path, String model, Map> agentModels, List searchPath) { path = path.toAbsolutePath().normalize(); Map skills = new TreeMap<>(); try (Stream dirs = Files.list(path)) { dirs.filter(Files::isDirectory) - .filter(d -> Files.exists(d.resolve("SKILL.md"))) - .sorted() - .forEach(d -> { - Map overrides = agentModels != null - ? agentModels.getOrDefault(d.getFileName().toString(), null) - : null; - Agent agent = skill(d, model, overrides, null, searchPath); - skills.put(d.getFileName().toString(), agent); - }); + .filter(d -> Files.exists(d.resolve("SKILL.md"))) + .sorted() + .forEach(d -> { + Map overrides = agentModels != null + ? agentModels.getOrDefault(d.getFileName().toString(), null) + : null; + Agent agent = skill(d, model, overrides, null, searchPath); + skills.put(d.getFileName().toString(), agent); + }); } catch (IOException e) { throw new SkillLoadError("Failed to list skills in " + path + ": " + e.getMessage(), e); } @@ -262,7 +259,9 @@ private static Map extractDefaultParams(String skillMd) { String trimmed = line.trim(); if (trimmed.isEmpty()) continue; if (trimmed.startsWith("default:") && current != null) { - params.put(current, parseScalar(trimmed.substring("default:".length()).trim())); + params.put( + current, + parseScalar(trimmed.substring("default:".length()).trim())); continue; } if (line.startsWith(" ") && !line.startsWith(" ") && trimmed.endsWith(":")) { @@ -338,16 +337,15 @@ private static Map loadAgentFiles(Path skillDir) { Map agentFiles = new TreeMap<>(); try (Stream files = Files.list(skillDir)) { files.filter(f -> f.getFileName().toString().endsWith("-agent.md")) - .sorted() - .forEach(f -> { - String agentName = f.getFileName().toString() - .replaceAll("-agent\\.md$", ""); - try { - agentFiles.put(agentName, Files.readString(f)); - } catch (IOException e) { - throw new SkillLoadError("Failed to read agent file " + f + ": " + e.getMessage(), e); - } - }); + .sorted() + .forEach(f -> { + String agentName = f.getFileName().toString().replaceAll("-agent\\.md$", ""); + try { + agentFiles.put(agentName, Files.readString(f)); + } catch (IOException e) { + throw new SkillLoadError("Failed to read agent file " + f + ": " + e.getMessage(), e); + } + }); } catch (IOException e) { throw new SkillLoadError("Failed to list agent files: " + e.getMessage(), e); } @@ -359,15 +357,13 @@ private static Map> loadScripts(Path skillDir) { Path scriptsDir = skillDir.resolve("scripts"); if (!Files.exists(scriptsDir)) return scripts; try (Stream files = Files.list(scriptsDir)) { - files.filter(Files::isRegularFile) - .sorted() - .forEach(f -> { - String stem = f.getFileName().toString().replaceAll("\\.[^.]+$", ""); - Map info = new LinkedHashMap<>(); - info.put("filename", f.getFileName().toString()); - info.put("language", detectLanguage(f)); - scripts.put(stem, info); - }); + files.filter(Files::isRegularFile).sorted().forEach(f -> { + String stem = f.getFileName().toString().replaceAll("\\.[^.]+$", ""); + Map info = new LinkedHashMap<>(); + info.put("filename", f.getFileName().toString()); + info.put("language", detectLanguage(f)); + scripts.put(stem, info); + }); } catch (IOException e) { throw new SkillLoadError("Failed to list scripts: " + e.getMessage(), e); } @@ -376,28 +372,26 @@ private static Map> loadScripts(Path skillDir) { private static List loadResourceFiles(Path skillDir) { List resources = new ArrayList<>(); - for (String subdir : new String[]{"references", "examples", "assets"}) { + for (String subdir : new String[] {"references", "examples", "assets"}) { Path d = skillDir.resolve(subdir); if (!Files.exists(d)) continue; try (Stream files = Files.walk(d)) { - files.filter(Files::isRegularFile) - .sorted() - .forEach(f -> resources.add(relativeSkillPath(skillDir, f))); + files.filter(Files::isRegularFile).sorted().forEach(f -> resources.add(relativeSkillPath(skillDir, f))); } catch (IOException e) { throw new SkillLoadError("Failed to list resource files in " + d + ": " + e.getMessage(), e); } } try (Stream files = Files.list(skillDir)) { files.filter(Files::isRegularFile) - .filter(f -> { - String name = f.getFileName().toString(); - return !"SKILL.md".equals(name) - && !"skill.yaml".equals(name) - && !"skill.toml".equals(name) - && !name.endsWith("-agent.md"); - }) - .sorted() - .forEach(f -> resources.add(relativeSkillPath(skillDir, f))); + .filter(f -> { + String name = f.getFileName().toString(); + return !"SKILL.md".equals(name) + && !"skill.yaml".equals(name) + && !"skill.toml".equals(name) + && !name.endsWith("-agent.md"); + }) + .sorted() + .forEach(f -> resources.add(relativeSkillPath(skillDir, f))); } catch (IOException e) { throw new SkillLoadError("Failed to list root resource files: " + e.getMessage(), e); } @@ -509,9 +503,17 @@ public static class SkillWorker { this.func = func; } - public String getName() { return name; } - public String getDescription() { return description; } - public Function, Object> getFunc() { return func; } + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Function, Object> getFunc() { + return func; + } } /** Create local workers for a skill agent's scripts and readable resource files. */ @@ -524,10 +526,10 @@ public static List createSkillWorkers(Agent agent) { String skillName = agent.getName(); Map config = agent.getFrameworkConfig(); Path skillPath = Paths.get((String) config.getOrDefault("skillPath", ".")) - .toAbsolutePath() - .normalize(); + .toAbsolutePath() + .normalize(); Map> scripts = - (Map>) config.getOrDefault("scripts", Collections.emptyMap()); + (Map>) config.getOrDefault("scripts", Collections.emptyMap()); List workers = new ArrayList<>(); for (Map.Entry> entry : scripts.entrySet()) { @@ -540,21 +542,19 @@ public static List createSkillWorkers(Agent agent) { String interpreter = INTERPRETERS.getOrDefault(info.getOrDefault("language", "bash"), "bash"); Path scriptPath = skillPath.resolve("scripts").resolve(filename).normalize(); workers.add(new SkillWorker( - workerName, - "Run " + toolName + " script from " + skillName + " skill", - input -> runScript(interpreter, scriptPath, stringValue(input.get("command"))) - )); + workerName, + "Run " + toolName + " script from " + skillName + " skill", + input -> runScript(interpreter, scriptPath, stringValue(input.get("command"))))); } Set allowedFiles = new HashSet<>((List) config.getOrDefault("resourceFiles", List.of())); Map skillSections = - (Map) config.getOrDefault("skillSections", Collections.emptyMap()); + (Map) config.getOrDefault("skillSections", Collections.emptyMap()); if (!allowedFiles.isEmpty()) { workers.add(new SkillWorker( - skillName + "__read_skill_file", - "Read resource files from " + skillName + " skill", - input -> readSkillFile(skillPath, allowedFiles, skillSections, stringValue(input.get("path"))) - )); + skillName + "__read_skill_file", + "Read resource files from " + skillName + " skill", + input -> readSkillFile(skillPath, allowedFiles, skillSections, stringValue(input.get("path"))))); } return workers; } @@ -570,9 +570,9 @@ private static String runScript(String interpreter, Path scriptPath, String comm pb.directory(scriptPath.getParent().toFile()); Process p = pb.start(); CompletableFuture stdoutFuture = - CompletableFuture.supplyAsync(() -> readStream(p.getInputStream())); + CompletableFuture.supplyAsync(() -> readStream(p.getInputStream())); CompletableFuture stderrFuture = - CompletableFuture.supplyAsync(() -> readStream(p.getErrorStream())); + CompletableFuture.supplyAsync(() -> readStream(p.getErrorStream())); boolean done = p.waitFor(300, TimeUnit.SECONDS); if (!done) { p.destroyForcibly(); diff --git a/sdk/java/src/main/java/ai/agentspan/skill/SkillLoadError.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/skill/SkillLoadError.java similarity index 90% rename from sdk/java/src/main/java/ai/agentspan/skill/SkillLoadError.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/skill/SkillLoadError.java index 17d5a6887..564d3084b 100644 --- a/sdk/java/src/main/java/ai/agentspan/skill/SkillLoadError.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/skill/SkillLoadError.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.skill; +package org.conductoross.conductor.ai.skill; /** * Thrown when a skill directory cannot be loaded. diff --git a/sdk/java/src/main/java/ai/agentspan/termination/AndTermination.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/AndTermination.java similarity index 80% rename from sdk/java/src/main/java/ai/agentspan/termination/AndTermination.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/termination/AndTermination.java index d95e1c45c..066bec612 100644 --- a/sdk/java/src/main/java/ai/agentspan/termination/AndTermination.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/AndTermination.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.termination; +package org.conductoross.conductor.ai.termination; import java.util.Arrays; import java.util.LinkedHashMap; @@ -19,8 +19,13 @@ public AndTermination(TerminationCondition left, TerminationCondition right) { this.right = right; } - public TerminationCondition getLeft() { return left; } - public TerminationCondition getRight() { return right; } + public TerminationCondition getLeft() { + return left; + } + + public TerminationCondition getRight() { + return right; + } @Override public Map toMap() { diff --git a/sdk/java/src/main/java/ai/agentspan/termination/MaxMessageTermination.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/MaxMessageTermination.java similarity index 94% rename from sdk/java/src/main/java/ai/agentspan/termination/MaxMessageTermination.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/termination/MaxMessageTermination.java index 19a5742c4..0e6d07752 100644 --- a/sdk/java/src/main/java/ai/agentspan/termination/MaxMessageTermination.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/MaxMessageTermination.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.termination; +package org.conductoross.conductor.ai.termination; import java.util.LinkedHashMap; import java.util.Map; diff --git a/sdk/java/src/main/java/ai/agentspan/termination/OrTermination.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/OrTermination.java similarity index 80% rename from sdk/java/src/main/java/ai/agentspan/termination/OrTermination.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/termination/OrTermination.java index fc39b5f0b..b76344aeb 100644 --- a/sdk/java/src/main/java/ai/agentspan/termination/OrTermination.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/OrTermination.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.termination; +package org.conductoross.conductor.ai.termination; import java.util.Arrays; import java.util.LinkedHashMap; @@ -19,8 +19,13 @@ public OrTermination(TerminationCondition left, TerminationCondition right) { this.right = right; } - public TerminationCondition getLeft() { return left; } - public TerminationCondition getRight() { return right; } + public TerminationCondition getLeft() { + return left; + } + + public TerminationCondition getRight() { + return right; + } @Override public Map toMap() { diff --git a/sdk/java/src/main/java/ai/agentspan/termination/StopMessageTermination.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/StopMessageTermination.java similarity index 95% rename from sdk/java/src/main/java/ai/agentspan/termination/StopMessageTermination.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/termination/StopMessageTermination.java index 6d7e4f97e..e21744cd0 100644 --- a/sdk/java/src/main/java/ai/agentspan/termination/StopMessageTermination.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/StopMessageTermination.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.termination; +package org.conductoross.conductor.ai.termination; import java.util.LinkedHashMap; import java.util.Map; diff --git a/sdk/java/src/main/java/ai/agentspan/termination/TerminationCondition.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/TerminationCondition.java similarity index 95% rename from sdk/java/src/main/java/ai/agentspan/termination/TerminationCondition.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/termination/TerminationCondition.java index f192366a5..dd1bdf684 100644 --- a/sdk/java/src/main/java/ai/agentspan/termination/TerminationCondition.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/TerminationCondition.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.termination; +package org.conductoross.conductor.ai.termination; import java.util.Map; diff --git a/sdk/java/src/main/java/ai/agentspan/termination/TerminationResult.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/TerminationResult.java similarity index 84% rename from sdk/java/src/main/java/ai/agentspan/termination/TerminationResult.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/termination/TerminationResult.java index c02da82bb..8492703de 100644 --- a/sdk/java/src/main/java/ai/agentspan/termination/TerminationResult.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/TerminationResult.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.termination; +package org.conductoross.conductor.ai.termination; /** * The result of evaluating a termination condition. @@ -33,8 +33,13 @@ public static TerminationResult continueRunning() { return new TerminationResult(false); } - public boolean isShouldTerminate() { return shouldTerminate; } - public String getReason() { return reason; } + public boolean isShouldTerminate() { + return shouldTerminate; + } + + public String getReason() { + return reason; + } @Override public String toString() { diff --git a/sdk/java/src/main/java/ai/agentspan/termination/TextMentionTermination.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/TextMentionTermination.java similarity index 96% rename from sdk/java/src/main/java/ai/agentspan/termination/TextMentionTermination.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/termination/TextMentionTermination.java index 1b9a4c331..54f1fc4da 100644 --- a/sdk/java/src/main/java/ai/agentspan/termination/TextMentionTermination.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/TextMentionTermination.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.termination; +package org.conductoross.conductor.ai.termination; import java.util.LinkedHashMap; import java.util.Map; diff --git a/sdk/java/src/main/java/ai/agentspan/termination/TokenUsageTermination.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/TokenUsageTermination.java similarity index 85% rename from sdk/java/src/main/java/ai/agentspan/termination/TokenUsageTermination.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/termination/TokenUsageTermination.java index dbb59dbf5..f9693b52d 100644 --- a/sdk/java/src/main/java/ai/agentspan/termination/TokenUsageTermination.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/termination/TokenUsageTermination.java @@ -1,7 +1,7 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.termination; +package org.conductoross.conductor.ai.termination; import java.util.LinkedHashMap; import java.util.Map; @@ -35,9 +35,17 @@ public static TokenUsageTermination ofCompletion(int maxCompletionTokens) { return new TokenUsageTermination(null, null, maxCompletionTokens); } - public Integer getMaxTotalTokens() { return maxTotalTokens; } - public Integer getMaxPromptTokens() { return maxPromptTokens; } - public Integer getMaxCompletionTokens() { return maxCompletionTokens; } + public Integer getMaxTotalTokens() { + return maxTotalTokens; + } + + public Integer getMaxPromptTokens() { + return maxPromptTokens; + } + + public Integer getMaxCompletionTokens() { + return maxCompletionTokens; + } @Override public Map toMap() { diff --git a/sdk/java/src/main/java/ai/agentspan/AgentTool.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/AgentTool.java similarity index 64% rename from sdk/java/src/main/java/ai/agentspan/AgentTool.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/tools/AgentTool.java index e09e27b5f..d28cd5df5 100644 --- a/sdk/java/src/main/java/ai/agentspan/AgentTool.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/AgentTool.java @@ -1,17 +1,18 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan; - -import ai.agentspan.internal.AgentConfigSerializer; -import ai.agentspan.model.ToolDef; -import ai.agentspan.skill.Skill; +package org.conductoross.conductor.ai.tools; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.internal.AgentConfigSerializer; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.skill.Skill; + /** * Factory for wrapping an {@link Agent} as a callable tool (agent_tool). * @@ -62,29 +63,30 @@ public static ToolDef from(Agent agent, String description) { Map config = new LinkedHashMap<>(); config.put("agentConfig", agentConfig); if ("skill".equals(agent.getFramework())) { - config.put("workerNames", Skill.createSkillWorkers(agent).stream() - .map(Skill.SkillWorker::getName) - .collect(Collectors.toList())); + config.put( + "workerNames", + Skill.createSkillWorkers(agent).stream() + .map(Skill.SkillWorker::getName) + .collect(Collectors.toList())); } Map inputSchema = Map.of( - "type", "object", - "properties", Map.of( - "request", Map.of( - "type", "string", - "description", "The request or question to send to this agent." - ) - ), - "required", List.of("request") - ); + "type", "object", + "properties", + Map.of( + "request", + Map.of( + "type", "string", + "description", "The request or question to send to this agent.")), + "required", List.of("request")); return ToolDef.builder() - .name(agent.getName()) - .description(description) - .toolType("agent_tool") - .inputSchema(inputSchema) - .config(config) - .agentRef(agent) - .build(); + .name(agent.getName()) + .description(description) + .toolType("agent_tool") + .inputSchema(inputSchema) + .config(config) + .agentRef(agent) + .build(); } } diff --git a/sdk/java/src/main/java/ai/agentspan/tools/HttpTool.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/HttpTool.java similarity index 80% rename from sdk/java/src/main/java/ai/agentspan/tools/HttpTool.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/tools/HttpTool.java index 7cb9c5f66..2430c38f5 100644 --- a/sdk/java/src/main/java/ai/agentspan/tools/HttpTool.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/HttpTool.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.tools; - -import ai.agentspan.model.ToolDef; +package org.conductoross.conductor.ai.tools; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.conductoross.conductor.ai.model.ToolDef; + /** * Builder for HTTP server-side tools. * @@ -46,10 +46,25 @@ public static class Builder { private Map inputSchema; private List credentials = new ArrayList<>(); - public Builder name(String name) { this.name = name; return this; } - public Builder description(String description) { this.description = description; return this; } - public Builder url(String url) { this.url = url; return this; } - public Builder method(String method) { this.method = method; return this; } + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder url(String url) { + this.url = url; + return this; + } + + public Builder method(String method) { + this.method = method; + return this; + } public Builder header(String key, String value) { this.headers.put(key, value); @@ -117,13 +132,13 @@ public ToolDef build() { } return new ToolDef.Builder() - .name(name) - .description(description) - .inputSchema(schema) - .toolType("http") - .config(config) - .credentials(credentials) - .build(); + .name(name) + .description(description) + .inputSchema(schema) + .toolType("http") + .config(config) + .credentials(credentials) + .build(); } } } diff --git a/sdk/java/src/main/java/ai/agentspan/tools/HumanTool.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/HumanTool.java similarity index 95% rename from sdk/java/src/main/java/ai/agentspan/tools/HumanTool.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/tools/HumanTool.java index abe395d31..7c9953c00 100644 --- a/sdk/java/src/main/java/ai/agentspan/tools/HumanTool.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/HumanTool.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.tools; - -import ai.agentspan.model.ToolDef; +package org.conductoross.conductor.ai.tools; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.conductoross.conductor.ai.model.ToolDef; + /** * Factory for a tool that pauses execution for human input (Conductor {@code HUMAN} task). * diff --git a/sdk/java/src/main/java/ai/agentspan/tools/McpTool.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/McpTool.java similarity index 77% rename from sdk/java/src/main/java/ai/agentspan/tools/McpTool.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/tools/McpTool.java index a644d4c22..df9e26019 100644 --- a/sdk/java/src/main/java/ai/agentspan/tools/McpTool.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/McpTool.java @@ -1,15 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.tools; - -import ai.agentspan.model.ToolDef; +package org.conductoross.conductor.ai.tools; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.conductoross.conductor.ai.model.ToolDef; + /** * Builder for MCP (Model Context Protocol) server-side tools. * @@ -44,10 +44,25 @@ public static class Builder { private List credentials = new ArrayList<>(); private Map additionalConfig = new HashMap<>(); - public Builder name(String name) { this.name = name; return this; } - public Builder description(String description) { this.description = description; return this; } - public Builder serverUrl(String serverUrl) { this.serverUrl = serverUrl; return this; } - public Builder toolName(String toolName) { this.toolName = toolName; return this; } + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder serverUrl(String serverUrl) { + this.serverUrl = serverUrl; + return this; + } + + public Builder toolName(String toolName) { + this.toolName = toolName; + return this; + } public Builder header(String key, String value) { this.headers.put(key, value); @@ -100,13 +115,13 @@ public ToolDef build() { } return new ToolDef.Builder() - .name(name) - .description(description) - .inputSchema(schema) - .toolType("mcp") - .config(config) - .credentials(credentials) - .build(); + .name(name) + .description(description) + .inputSchema(schema) + .toolType("mcp") + .config(config) + .credentials(credentials) + .build(); } } } diff --git a/sdk/java/src/main/java/ai/agentspan/tools/MediaTools.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/MediaTools.java similarity index 89% rename from sdk/java/src/main/java/ai/agentspan/tools/MediaTools.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/tools/MediaTools.java index d14f73199..84d4b03fd 100644 --- a/sdk/java/src/main/java/ai/agentspan/tools/MediaTools.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/MediaTools.java @@ -1,16 +1,15 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.tools; +package org.conductoross.conductor.ai.tools; -import ai.agentspan.model.ToolDef; - -import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.conductoross.conductor.ai.model.ToolDef; + /** * Factory methods for server-side media generation tools. * @@ -28,14 +27,13 @@ public class MediaTools { private MediaTools() {} /** Create an image-generation tool (Conductor {@code GENERATE_IMAGE} task). */ - public static ToolDef imageTool(String name, String description, - String llmProvider, String model) { + public static ToolDef imageTool(String name, String description, String llmProvider, String model) { return imageTool(name, description, llmProvider, model, null); } /** Create an image-generation tool with a custom input schema. */ - public static ToolDef imageTool(String name, String description, - String llmProvider, String model, Map inputSchema) { + public static ToolDef imageTool( + String name, String description, String llmProvider, String model, Map inputSchema) { if (inputSchema == null) { inputSchema = new LinkedHashMap<>(); inputSchema.put("type", "object"); @@ -54,14 +52,13 @@ public static ToolDef imageTool(String name, String description, } /** Create an audio / text-to-speech tool (Conductor {@code GENERATE_AUDIO} task). */ - public static ToolDef audioTool(String name, String description, - String llmProvider, String model) { + public static ToolDef audioTool(String name, String description, String llmProvider, String model) { return audioTool(name, description, llmProvider, model, null); } /** Create an audio / text-to-speech tool with a custom input schema. */ - public static ToolDef audioTool(String name, String description, - String llmProvider, String model, Map inputSchema) { + public static ToolDef audioTool( + String name, String description, String llmProvider, String model, Map inputSchema) { if (inputSchema == null) { inputSchema = new LinkedHashMap<>(); inputSchema.put("type", "object"); @@ -80,14 +77,13 @@ public static ToolDef audioTool(String name, String description, } /** Create a video-generation tool (Conductor {@code GENERATE_VIDEO} task). */ - public static ToolDef videoTool(String name, String description, - String llmProvider, String model) { + public static ToolDef videoTool(String name, String description, String llmProvider, String model) { return videoTool(name, description, llmProvider, model, null); } /** Create a video-generation tool with a custom input schema. */ - public static ToolDef videoTool(String name, String description, - String llmProvider, String model, Map inputSchema) { + public static ToolDef videoTool( + String name, String description, String llmProvider, String model, Map inputSchema) { if (inputSchema == null) { inputSchema = new LinkedHashMap<>(); inputSchema.put("type", "object"); @@ -136,9 +132,13 @@ public static ToolDef pdfTool(String name, String description, Map inputSchema) { Map config = new LinkedHashMap<>(); config.put("taskType", taskType); diff --git a/sdk/java/src/main/java/ai/agentspan/tools/PdfTool.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/PdfTool.java similarity index 94% rename from sdk/java/src/main/java/ai/agentspan/tools/PdfTool.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/tools/PdfTool.java index 9836f97bf..2640a9f27 100644 --- a/sdk/java/src/main/java/ai/agentspan/tools/PdfTool.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/PdfTool.java @@ -1,14 +1,14 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.tools; - -import ai.agentspan.model.ToolDef; +package org.conductoross.conductor.ai.tools; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.conductoross.conductor.ai.model.ToolDef; + /** * Factory for the server-side PDF-generation tool (Conductor {@code GENERATE_PDF} task). * @@ -64,8 +64,8 @@ public static ToolDef create(String name, String description, Map inputSchema, Map defaults) { + public static ToolDef create( + String name, String description, Map inputSchema, Map defaults) { var schema = inputSchema != null ? inputSchema : defaultInputSchema(); var config = new LinkedHashMap(); diff --git a/sdk/java/src/main/java/ai/agentspan/RagTools.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/RagTools.java similarity index 77% rename from sdk/java/src/main/java/ai/agentspan/RagTools.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/tools/RagTools.java index 9b3925e6b..98dfc60a1 100644 --- a/sdk/java/src/main/java/ai/agentspan/RagTools.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/RagTools.java @@ -1,12 +1,13 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan; +package org.conductoross.conductor.ai.tools; -import ai.agentspan.model.ToolDef; import java.util.LinkedHashMap; import java.util.Map; +import org.conductoross.conductor.ai.model.ToolDef; + /** * Factory methods for RAG (Retrieval-Augmented Generation) tools. * @@ -40,21 +41,30 @@ private RagTools() {} * @param maxResults maximum number of results to return * @return a ToolDef with toolType "rag_search" */ - public static ToolDef searchTool(String name, String description, - String vectorDb, String index, - String embeddingModelProvider, String embeddingModel, + public static ToolDef searchTool( + String name, + String description, + String vectorDb, + String index, + String embeddingModelProvider, + String embeddingModel, int maxResults) { - return searchTool(name, description, vectorDb, index, - embeddingModelProvider, embeddingModel, "default_ns", maxResults); + return searchTool( + name, description, vectorDb, index, embeddingModelProvider, embeddingModel, "default_ns", maxResults); } /** * Create a vector search tool with explicit namespace. */ - public static ToolDef searchTool(String name, String description, - String vectorDb, String index, - String embeddingModelProvider, String embeddingModel, - String namespace, int maxResults) { + public static ToolDef searchTool( + String name, + String description, + String vectorDb, + String index, + String embeddingModelProvider, + String embeddingModel, + String namespace, + int maxResults) { Map inputSchema = new LinkedHashMap<>(); inputSchema.put("type", "object"); Map props = new LinkedHashMap<>(); @@ -94,19 +104,26 @@ public static ToolDef searchTool(String name, String description, * @param embeddingModel embedding model name (e.g. "text-embedding-3-small") * @return a ToolDef with toolType "rag_index" */ - public static ToolDef indexTool(String name, String description, - String vectorDb, String index, - String embeddingModelProvider, String embeddingModel) { - return indexTool(name, description, vectorDb, index, - embeddingModelProvider, embeddingModel, "default_ns"); + public static ToolDef indexTool( + String name, + String description, + String vectorDb, + String index, + String embeddingModelProvider, + String embeddingModel) { + return indexTool(name, description, vectorDb, index, embeddingModelProvider, embeddingModel, "default_ns"); } /** * Create a vector index tool with explicit namespace. */ - public static ToolDef indexTool(String name, String description, - String vectorDb, String index, - String embeddingModelProvider, String embeddingModel, + public static ToolDef indexTool( + String name, + String description, + String vectorDb, + String index, + String embeddingModelProvider, + String embeddingModel, String namespace) { Map inputSchema = new LinkedHashMap<>(); inputSchema.put("type", "object"); diff --git a/sdk/java/src/main/java/ai/agentspan/tools/WaitForMessageTool.java b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/WaitForMessageTool.java similarity index 95% rename from sdk/java/src/main/java/ai/agentspan/tools/WaitForMessageTool.java rename to sdk/java/src/main/java/org/conductoross/conductor/ai/tools/WaitForMessageTool.java index d4655ba1d..4f5f78c5d 100644 --- a/sdk/java/src/main/java/ai/agentspan/tools/WaitForMessageTool.java +++ b/sdk/java/src/main/java/org/conductoross/conductor/ai/tools/WaitForMessageTool.java @@ -1,13 +1,13 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.tools; - -import ai.agentspan.model.ToolDef; +package org.conductoross.conductor.ai.tools; import java.util.LinkedHashMap; import java.util.Map; +import org.conductoross.conductor.ai.model.ToolDef; + /** * Factory for a tool that dequeues messages from the Workflow Message Queue * (Conductor {@code PULL_WORKFLOW_MESSAGES} task). diff --git a/sdk/java/src/test/java/ai/agentspan/CredentialsTest.java b/sdk/java/src/test/java/ai/agentspan/CredentialsTest.java deleted file mode 100644 index 8a1a1eb9f..000000000 --- a/sdk/java/src/test/java/ai/agentspan/CredentialsTest.java +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) 2025 Agentspan -// Licensed under the MIT License. See LICENSE file in the project root for details. - -package ai.agentspan; - -import static org.junit.jupiter.api.Assertions.*; - -import ai.agentspan.exceptions.CredentialNotFoundException; - -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -/** - * Unit tests for the {@link Credentials} thread-local accessor. - * - *

    Java is tier-1-only per the SDK secret-injection contract; this is the - * only injection mechanism. Tests cover:

    - *
      - *
    • Reading a set value via {@link Credentials#get(String)}.
    • - *
    • Missing context → typed exception (not silent null).
    • - *
    • Per-thread isolation — concurrent threads see independent contexts.
    • - *
    • {@code clearForCall} cleanly removes the ThreadLocal.
    • - *
    - */ -class CredentialsTest { - - @AfterEach - void cleanUp() { - Credentials.clearForCall(); - } - - @Test - void get_returnsSetValue() { - Credentials.setForCall(Map.of("OPENAI_API_KEY", "sk-test-123")); - assertEquals("sk-test-123", Credentials.get("OPENAI_API_KEY")); - } - - @Test - void get_outsideToolCall_throwsCredentialNotFound() { - Credentials.clearForCall(); - CredentialNotFoundException ex = assertThrows( - CredentialNotFoundException.class, () -> Credentials.get("ANYTHING")); - assertTrue(ex.getMessage().contains("outside a credential-aware"), - "message should explain why no context: " + ex.getMessage()); - } - - @Test - void get_unknownName_throwsCredentialNotFound() { - Credentials.setForCall(Map.of("KNOWN", "value")); - assertThrows(CredentialNotFoundException.class, () -> Credentials.get("UNKNOWN")); - } - - @Test - void getOrNull_returnsNullSilentlyOutsideContext() { - Credentials.clearForCall(); - assertNull(Credentials.getOrNull("ANY")); - } - - @Test - void all_returnsUnmodifiableViewOfCurrentContext() { - Credentials.setForCall(Map.of("A", "1", "B", "2")); - Map view = Credentials.all(); - assertEquals(2, view.size()); - assertEquals("1", view.get("A")); - assertThrows(UnsupportedOperationException.class, () -> view.put("C", "3")); - } - - @Test - void perThreadIsolation_concurrentThreadsDontShareContext() throws Exception { - // Audit-style: two threads each set a different value for the same key, - // both read back inside their own thread. Neither should observe the - // other's value. Mirrors Python's contextvars-isolation test and TS's - // runWithCredentialContext isolation test. - AtomicReference threadAObserved = new AtomicReference<>(); - AtomicReference threadBObserved = new AtomicReference<>(); - CountDownLatch bothSet = new CountDownLatch(2); - CountDownLatch bothRead = new CountDownLatch(2); - - Runnable threadA = () -> { - try { - Credentials.setForCall(Map.of("KEY", "value-A")); - bothSet.countDown(); - bothSet.await(5, TimeUnit.SECONDS); // wait for B to also have set - threadAObserved.set(Credentials.get("KEY")); - bothRead.countDown(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } finally { - Credentials.clearForCall(); - } - }; - - Runnable threadB = () -> { - try { - Credentials.setForCall(Map.of("KEY", "value-B")); - bothSet.countDown(); - bothSet.await(5, TimeUnit.SECONDS); - threadBObserved.set(Credentials.get("KEY")); - bothRead.countDown(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } finally { - Credentials.clearForCall(); - } - }; - - Thread tA = new Thread(threadA, "test-A"); - Thread tB = new Thread(threadB, "test-B"); - tA.start(); - tB.start(); - assertTrue(bothRead.await(10, TimeUnit.SECONDS), "both threads must finish reading"); - tA.join(1000); - tB.join(1000); - - // ThreadLocal isolates: each thread sees only its own value. - assertEquals("value-A", threadAObserved.get()); - assertEquals("value-B", threadBObserved.get()); - } - - @Test - void setForCall_withEmptyMap_clearsContext() { - Credentials.setForCall(Map.of("X", "y")); - Credentials.setForCall(Map.of()); - assertNull(Credentials.getOrNull("X")); - } - - @Test - void setForCall_withNull_clearsContext() { - Credentials.setForCall(Map.of("X", "y")); - Credentials.setForCall(null); - assertNull(Credentials.getOrNull("X")); - } -} diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/AgentAnnotationTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/AgentAnnotationTest.java new file mode 100644 index 000000000..9fbdb2ee1 --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/AgentAnnotationTest.java @@ -0,0 +1,610 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.conductoross.conductor.ai.annotations.AgentDef; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.model.GuardrailResult; +import org.conductoross.conductor.ai.model.PromptTemplate; +import org.conductoross.conductor.ai.model.ToolDef; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link AgentDef @AgentDef}-annotated + * method resolution via {@link Agent#fromInstance(Object)}. + */ +class AgentAnnotationTest { + + // ── Fixtures ───────────────────────────────────────────────────────── + + static class BasicAgent { + @AgentDef(model = "openai/gpt-4o", instructions = "You are a helpful assistant.") + public void assistant() {} + } + + static class DynamicInstructions { + @AgentDef(model = "openai/gpt-4o", instructions = "static fallback") + public String weatherbot() { + return "You are a weather assistant."; + } + } + + static class EmptyDynamicInstructions { + @AgentDef(model = "openai/gpt-4o", instructions = "static fallback") + public String agentWithFallback() { + return ""; + } + } + + static class WithTools { + @Tool(description = "Get weather for a city") + public String getWeather(String city) { + return "Sunny, 72F in " + city; + } + + @Tool(name = "get_time", description = "Get current time") + public String getTime() { + return "12:00"; + } + + @AgentDef(model = "openai/gpt-4o") + public String allTools() { + return "You can use all tools."; + } + + @AgentDef( + model = "openai/gpt-4o", + tools = {"get_time"}) + public String oneTool() { + return "You can tell the time."; + } + + @AgentDef( + model = "openai/gpt-4o", + tools = {}) + public String noTools() { + return "No tools for you."; + } + } + + static class UnknownToolName { + @Tool(description = "Get weather") + public String getWeather(String city) { + return "Sunny"; + } + + @AgentDef( + model = "openai/gpt-4o", + tools = {"does_not_exist"}) + public void broken() {} + } + + static class WithGuardrails { + @org.conductoross.conductor.ai.annotations.GuardrailDef + public GuardrailResult noPii(String output) { + return GuardrailResult.pass(); + } + + @AgentDef(model = "openai/gpt-4o") + public void guarded() {} + + @AgentDef( + model = "openai/gpt-4o", + guardrails = {}) + public void unguarded() {} + } + + static class MultiAgent { + @AgentDef(instructions = "Handle billing questions.") + public void billing() {} + + @AgentDef(model = "anthropic/claude-sonnet-4-6", instructions = "Handle technical support.") + public void support() {} + + @AgentDef( + model = "openai/gpt-4o", + instructions = "Route customer questions.", + agents = {"billing", "support"}, + strategy = Strategy.HANDOFF) + public void triage() {} + } + + static class UnknownSubAgent { + @AgentDef( + model = "openai/gpt-4o", + agents = {"ghost"}) + public void parent() {} + } + + static class CyclicAgents { + @AgentDef( + model = "openai/gpt-4o", + agents = {"b"}) + public void a() {} + + @AgentDef( + model = "openai/gpt-4o", + agents = {"a"}) + public void b() {} + } + + static class CustomAttributes { + @AgentDef( + name = "custom_name", + model = "openai/gpt-4o", + maxTurns = 5, + maxTokens = 1024, + temperature = 0.2, + credentials = {"OPENAI_API_KEY"}, + contextWindowBudget = 50000) + public void ignoredMethodName() {} + } + + static class BuilderCustomizer { + @Tool(description = "Search the web") + public String search(String query) { + return "results"; + } + + @AgentDef(model = "openai/gpt-4o", instructions = "You are a researcher.") + public void researcher(Agent.Builder builder) { + builder.termination(new org.conductoross.conductor.ai.termination.MaxMessageTermination(5)) + .synthesize(false); + } + + @AgentDef(model = "openai/gpt-4o", instructions = "static") + public String dynamicWithCustomizer(Agent.Builder builder) { + builder.maskedFields("ssn"); + return "dynamic instructions"; + } + } + + static class StrategyViaCustomizer { + @AgentDef(instructions = "Write a draft.") + public void writer() {} + + @AgentDef( + model = "openai/gpt-4o", + strategy = Strategy.SEQUENTIAL, + tools = {}) + public void pipeline(Agent.Builder builder) { + builder.agents( + Agent.fromInstance(this, "writer"), Agent.fromInstance(new CrossClassSpecialist(), "editor")); + } + } + + static class CrossClassSpecialist { + @AgentDef(model = "anthropic/claude-sonnet-4-6", instructions = "Edit the draft.") + public void editor() {} + } + + static class TwoParameters { + @AgentDef(model = "openai/gpt-4o") + public void bad(Agent.Builder builder, String extra) {} + } + + static class PromptTemplateReturn { + @AgentDef(model = "openai/gpt-4o") + public PromptTemplate templated() { + return new PromptTemplate("customer-support", Map.of("tone", "friendly")); + } + } + + static class FullAgentFactory { + @AgentDef + public Agent handbuilt() { + return Agent.builder() + .name("handbuilt") + .model("openai/gpt-4o") + .instructions("Factory built.") + .maxTurns(3) + .build(); + } + } + + static class FluentBuilderReturn { + @AgentDef(model = "openai/gpt-4o", instructions = "fluent") + public Agent.Builder fluent(Agent.Builder builder) { + return builder.maxTurns(3); + } + } + + static class NoArgBuilderFactory { + @AgentDef + public Agent.Builder scratch() { + return Agent.builder().name("scratch_agent").model("openai/gpt-4o"); + } + } + + static class FactoryWithAttributes { + @AgentDef(model = "openai/gpt-4o") + public Agent bad() { + return Agent.builder().name("x").build(); + } + } + + static class NullFactory { + @AgentDef + public Agent nothing() { + return null; + } + } + + static class LazyInstructions { + int calls = 0; + + @AgentDef(model = "openai/gpt-4o") + public String counterbot() { + calls++; + return "version " + calls; + } + } + + static class EagerWithBuilder { + int calls = 0; + + @AgentDef(model = "openai/gpt-4o") + public String eager(Agent.Builder builder) { + calls++; + return "eager " + calls; + } + } + + static class HiddenAgent { + @AgentDef(model = "openai/gpt-4o") + private void secret() {} + } + + static class BaseBot { + @AgentDef(model = "openai/gpt-4o") + public String bot() { + return "base instructions"; + } + } + + /** Simulates a CGLIB-style proxy: overrides the annotated method without re-annotating. */ + static class ProxyBot extends BaseBot { + @Override + public String bot() { + return "proxied instructions"; + } + } + + static class PlainChild extends BaseBot {} + + interface BotDefs { + @AgentDef(model = "openai/gpt-4o", instructions = "From interface.") + default void ifaceBot() {} + } + + static class ImplBot implements BotDefs {} + + static class ToolAndAgent { + @Tool(description = "x") + @AgentDef(model = "openai/gpt-4o") + public String both() { + return "x"; + } + } + + static class BadReturnType { + @AgentDef(model = "openai/gpt-4o") + public int badReturn() { + return 42; + } + } + + static class BadParameters { + @AgentDef(model = "openai/gpt-4o") + public String badParams(String unexpected) { + return "instructions"; + } + } + + // ── Tests ──────────────────────────────────────────────────────────── + + @Test + void basicAgentUsesMethodNameAndStaticInstructions() { + List agents = Agent.fromInstance(new BasicAgent()); + assertEquals(1, agents.size()); + Agent agent = agents.get(0); + assertEquals("assistant", agent.getName()); + assertEquals("openai/gpt-4o", agent.getModel()); + assertEquals("You are a helpful assistant.", agent.getInstructions()); + assertEquals(25, agent.getMaxTurns()); + } + + @Test + void stringReturningMethodProvidesDynamicInstructions() { + Agent agent = Agent.fromInstance(new DynamicInstructions(), "weatherbot"); + assertEquals("You are a weather assistant.", agent.getInstructions()); + } + + @Test + void emptyDynamicInstructionsFallBackToAttribute() { + Agent agent = Agent.fromInstance(new EmptyDynamicInstructions(), "agentWithFallback"); + assertEquals("static fallback", agent.getInstructions()); + } + + @Test + void allToolMethodsAttachedByDefault() { + Agent agent = Agent.fromInstance(new WithTools(), "allTools"); + assertEquals(2, agent.getTools().size()); + List names = agent.getTools().stream().map(ToolDef::getName).toList(); + assertTrue(names.contains("getWeather")); + assertTrue(names.contains("get_time")); + } + + @Test + void toolsFilteredByName() { + Agent agent = Agent.fromInstance(new WithTools(), "oneTool"); + assertEquals(1, agent.getTools().size()); + assertEquals("get_time", agent.getTools().get(0).getName()); + } + + @Test + void emptyToolsArrayAttachesNoTools() { + Agent agent = Agent.fromInstance(new WithTools(), "noTools"); + assertTrue(agent.getTools().isEmpty()); + } + + @Test + void unknownToolNameThrows() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new UnknownToolName())); + assertTrue(e.getMessage().contains("does_not_exist")); + } + + @Test + void guardrailsAttachedByDefaultAndFilterable() { + Agent guarded = Agent.fromInstance(new WithGuardrails(), "guarded"); + assertEquals(1, guarded.getGuardrails().size()); + assertEquals("noPii", guarded.getGuardrails().get(0).getName()); + + Agent unguarded = Agent.fromInstance(new WithGuardrails(), "unguarded"); + assertTrue(unguarded.getGuardrails().isEmpty()); + } + + @Test + void subAgentsResolvedByNameWithModelInheritance() { + Agent triage = Agent.fromInstance(new MultiAgent(), "triage"); + assertEquals(Strategy.HANDOFF, triage.getStrategy()); + assertEquals(2, triage.getAgents().size()); + + Agent billing = triage.getAgents().get(0); + assertEquals("billing", billing.getName()); + // billing has no model — inherits the parent's + assertEquals("openai/gpt-4o", billing.getModel()); + + Agent support = triage.getAgents().get(1); + // support declares its own model — no inheritance + assertEquals("anthropic/claude-sonnet-4-6", support.getModel()); + } + + @Test + void topLevelResolutionReturnsAllAgents() { + List agents = Agent.fromInstance(new MultiAgent()); + assertEquals(3, agents.size()); + } + + @Test + void unknownSubAgentNameThrows() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new UnknownSubAgent())); + assertTrue(e.getMessage().contains("ghost")); + } + + @Test + void cyclicSubAgentsThrow() { + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new CyclicAgents())); + } + + @Test + void annotationAttributesMapToAgentFields() { + Agent agent = Agent.fromInstance(new CustomAttributes(), "custom_name"); + assertEquals("custom_name", agent.getName()); + assertEquals(5, agent.getMaxTurns()); + assertEquals(1024, agent.getMaxTokens()); + assertEquals(0.2, agent.getTemperature()); + assertEquals(List.of("OPENAI_API_KEY"), agent.getCredentials()); + assertEquals(50000, agent.getContextWindowBudget()); + } + + @Test + void unsetOptionalsStayNull() { + Agent agent = Agent.fromInstance(new BasicAgent(), "assistant"); + assertNull(agent.getMaxTokens()); + assertNull(agent.getTemperature()); + assertNull(agent.getContextWindowBudget()); + } + + @Test + void customizerReceivesPrepopulatedBuilder() { + Agent agent = Agent.fromInstance(new BuilderCustomizer(), "researcher"); + // customizations from the method body + assertTrue(agent.getTermination() instanceof org.conductoross.conductor.ai.termination.MaxMessageTermination); + assertTrue(!agent.isSynthesize()); + // pre-populated state from the annotation survives + assertEquals("openai/gpt-4o", agent.getModel()); + assertEquals("You are a researcher.", agent.getInstructions()); + assertEquals(1, agent.getTools().size()); + assertEquals("search", agent.getTools().get(0).getName()); + } + + @Test + void returnedStringWinsOverCustomizerAndAttribute() { + Agent agent = Agent.fromInstance(new BuilderCustomizer(), "dynamicWithCustomizer"); + assertEquals("dynamic instructions", agent.getInstructions()); + assertEquals(List.of("ssn"), agent.getMaskedFields()); + } + + @Test + void strategyAppliesWhenSubAgentsAddedViaCustomizer() { + Agent pipeline = Agent.fromInstance(new StrategyViaCustomizer(), "pipeline"); + assertEquals(Strategy.SEQUENTIAL, pipeline.getStrategy()); + assertEquals(2, pipeline.getAgents().size()); + // cross-class sub-agent resolved from another instance + assertEquals("editor", pipeline.getAgents().get(1).getName()); + assertEquals("anthropic/claude-sonnet-4-6", pipeline.getAgents().get(1).getModel()); + } + + @Test + void extraParametersBeyondBuilderThrow() { + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new TwoParameters())); + } + + // ── Return-type ladder ─────────────────────────────────────────────── + + @Test + void promptTemplateReturnSetsInstructionsTemplate() { + Agent agent = Agent.fromInstance(new PromptTemplateReturn(), "templated"); + assertEquals("customer-support", agent.getInstructionsTemplate().getName()); + assertEquals("friendly", agent.getInstructionsTemplate().getVariables().get("tone")); + } + + @Test + void agentReturningMethodIsFullFactory() { + // discovery key is the method name; the agent keeps the name set by the factory + Agent agent = Agent.fromInstance(new FullAgentFactory(), "handbuilt"); + assertEquals("handbuilt", agent.getName()); + assertEquals("Factory built.", agent.getInstructions()); + assertEquals(3, agent.getMaxTurns()); + } + + @Test + void returnedBuilderIsBuiltWithAnnotationDefaults() { + Agent agent = Agent.fromInstance(new FluentBuilderReturn(), "fluent"); + assertEquals(3, agent.getMaxTurns()); + assertEquals("openai/gpt-4o", agent.getModel()); + assertEquals("fluent", agent.getInstructions()); + } + + @Test + void noArgBuilderReturnIsFactoryBuiltAsIs() { + Agent agent = Agent.fromInstance(new NoArgBuilderFactory(), "scratch"); + assertEquals("scratch_agent", agent.getName()); + assertEquals("openai/gpt-4o", agent.getModel()); + } + + @Test + void pureFactoryWithNonDefaultAttributesThrows() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new FactoryWithAttributes())); + assertTrue(e.getMessage().contains("model")); + } + + @Test + void factoryReturningNullThrows() { + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new NullFactory())); + } + + // ── Lazy instructions (evaluated per access, i.e. per run submission) ─ + + @Test + void noArgStringInstructionsAreLazyAndReevaluated() { + LazyInstructions fixture = new LazyInstructions(); + Agent agent = Agent.fromInstance(fixture, "counterbot"); + assertEquals(0, fixture.calls); + assertEquals("version 1", agent.getInstructions()); + assertEquals("version 2", agent.getInstructions()); + assertEquals(2, fixture.calls); + } + + @Test + void supplierInstructionsOnBuilderAreReevaluatedPerAccess() { + AtomicInteger calls = new AtomicInteger(); + Agent agent = Agent.builder() + .name("lazy") + .model("openai/gpt-4o") + .instructions(() -> "prompt v" + calls.incrementAndGet()) + .build(); + assertEquals("prompt v1", agent.getInstructions()); + assertEquals("prompt v2", agent.getInstructions()); + } + + @Test + void lazyInstructionsReachTheSerializedConfig() { + LazyInstructions fixture = new LazyInstructions(); + Agent agent = Agent.fromInstance(fixture, "counterbot"); + var serializer = new org.conductoross.conductor.ai.internal.AgentConfigSerializer(); + assertEquals("version 1", serializer.serialize(agent).get("instructions")); + assertEquals("version 2", serializer.serialize(agent).get("instructions")); + } + + @Test + void builderParamStringInstructionsStayEager() { + EagerWithBuilder fixture = new EagerWithBuilder(); + Agent agent = Agent.fromInstance(fixture, "eager"); + assertEquals(1, fixture.calls); + assertEquals("eager 1", agent.getInstructions()); + assertEquals("eager 1", agent.getInstructions()); + assertEquals(1, fixture.calls); + } + + // ── Discovery: visibility, inheritance, proxies ────────────────────── + + @Test + void nonPublicAgentDefMethodThrowsInsteadOfSilentIgnore() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new HiddenAgent())); + assertTrue(e.getMessage().contains("public")); + assertTrue(e.getMessage().contains("secret")); + } + + @Test + void unannotatedOverrideStillDiscoveredWithVirtualDispatch() { + // a subclass (e.g. CGLIB proxy) overriding without re-annotating must not + // hide the agent, and invocation must dispatch to the override + Agent agent = Agent.fromInstance(new ProxyBot(), "bot"); + assertEquals("proxied instructions", agent.getInstructions()); + } + + @Test + void inheritedAnnotatedMethodDiscovered() { + assertEquals( + "base instructions", Agent.fromInstance(new PlainChild(), "bot").getInstructions()); + assertEquals( + "base instructions", Agent.fromInstance(new BaseBot(), "bot").getInstructions()); + } + + @Test + void interfaceDefaultMethodDiscovered() { + Agent agent = Agent.fromInstance(new ImplBot(), "ifaceBot"); + assertEquals("From interface.", agent.getInstructions()); + } + + @Test + void agentDefCombinedWithToolThrows() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new ToolAndAgent())); + assertTrue(e.getMessage().contains("@Tool")); + } + + @Test + void nonStringNonVoidReturnTypeThrows() { + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new BadReturnType())); + } + + @Test + void stringReturningMethodWithParametersThrows() { + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new BadParameters())); + } + + @Test + void missingNamedAgentThrows() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> Agent.fromInstance(new BasicAgent(), "nope")); + assertTrue(e.getMessage().contains("nope")); + } +} diff --git a/sdk/java/src/test/java/ai/agentspan/AgentBuilderTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/AgentBuilderTest.java similarity index 71% rename from sdk/java/src/test/java/ai/agentspan/AgentBuilderTest.java rename to sdk/java/src/test/java/org/conductoross/conductor/ai/AgentBuilderTest.java index 002d064ad..8aada7a0a 100644 --- a/sdk/java/src/test/java/ai/agentspan/AgentBuilderTest.java +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/AgentBuilderTest.java @@ -1,23 +1,21 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan; - -import ai.agentspan.annotations.Tool; -import ai.agentspan.enums.Strategy; -import ai.agentspan.internal.AgentConfigSerializer; -import ai.agentspan.internal.ToolRegistry; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.GuardrailResult; -import ai.agentspan.model.ToolDef; -import ai.agentspan.termination.MaxMessageTermination; -import ai.agentspan.termination.TextMentionTermination; -import org.junit.jupiter.api.Test; +package org.conductoross.conductor.ai; + +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.internal.AgentConfigSerializer; +import org.conductoross.conductor.ai.internal.ToolRegistry; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.termination.MaxMessageTermination; +import org.conductoross.conductor.ai.termination.TextMentionTermination; +import org.junit.jupiter.api.Test; /** * Unit tests for Agent builder, ToolRegistry, and AgentConfigSerializer. @@ -29,10 +27,10 @@ class AgentBuilderTest { @Test void testBasicAgentBuilder() { Agent agent = Agent.builder() - .name("test_agent") - .model("openai/gpt-4o") - .instructions("You are a test agent.") - .build(); + .name("test_agent") + .model("openai/gpt-4o") + .instructions("You are a test agent.") + .build(); assertEquals("test_agent", agent.getName()); assertEquals("openai/gpt-4o", agent.getModel()); @@ -43,24 +41,20 @@ void testBasicAgentBuilder() { @Test void testExternalAgent() { - Agent external = Agent.builder() - .name("external_agent") - .build(); // No model = external + Agent external = Agent.builder().name("external_agent").build(); // No model = external assertTrue(external.isExternal()); } @Test void testInvalidAgentName() { - assertThrows(IllegalArgumentException.class, () -> - Agent.builder().name("123invalid").build() - ); - assertThrows(IllegalArgumentException.class, () -> - Agent.builder().name("").build() - ); - assertThrows(IllegalArgumentException.class, () -> - Agent.builder().name(null).build() - ); + assertThrows( + IllegalArgumentException.class, + () -> Agent.builder().name("123invalid").build()); + assertThrows( + IllegalArgumentException.class, () -> Agent.builder().name("").build()); + assertThrows( + IllegalArgumentException.class, () -> Agent.builder().name(null).build()); } @Test @@ -73,9 +67,9 @@ void testValidAgentNames() { @Test void testAgentMaxTurnsValidation() { - assertThrows(IllegalArgumentException.class, () -> - Agent.builder().name("test").maxTurns(0).build() - ); + assertThrows( + IllegalArgumentException.class, + () -> Agent.builder().name("test").maxTurns(0).build()); } @Test @@ -117,9 +111,9 @@ void testToolRegistryDiscovery() { assertEquals(2, toolDefs.size()); ToolDef weatherTool = toolDefs.stream() - .filter(t -> t.getName().equals("get_weather")) - .findFirst() - .orElseThrow(); + .filter(t -> t.getName().equals("get_weather")) + .findFirst() + .orElseThrow(); assertEquals("get_weather", weatherTool.getName()); assertEquals("Get weather for a city", weatherTool.getDescription()); @@ -133,20 +127,20 @@ void testToolExecution() { List toolDefs = ToolRegistry.fromInstance(tools); ToolDef weatherTool = toolDefs.stream() - .filter(t -> t.getName().equals("get_weather")) - .findFirst() - .orElseThrow(); + .filter(t -> t.getName().equals("get_weather")) + .findFirst() + .orElseThrow(); // Use the actual parameter name from the schema @SuppressWarnings("unchecked") - Map props = (Map) weatherTool.getInputSchema().get("properties"); + Map props = + (Map) weatherTool.getInputSchema().get("properties"); String paramName = props.keySet().iterator().next(); Map input = Map.of(paramName, "Paris"); Object result = weatherTool.getFunc().apply(input); - assertTrue(result.toString().contains("Paris"), - "Expected result to contain 'Paris' but was: " + result); + assertTrue(result.toString().contains("Paris"), "Expected result to contain 'Paris' but was: " + result); } @Test @@ -155,9 +149,9 @@ void testToolSchemaGeneration() { List toolDefs = ToolRegistry.fromInstance(tools); ToolDef addTool = toolDefs.stream() - .filter(t -> t.getName().equals("add_numbers")) - .findFirst() - .orElseThrow(); + .filter(t -> t.getName().equals("add_numbers")) + .findFirst() + .orElseThrow(); Map schema = addTool.getInputSchema(); assertNotNull(schema); @@ -174,11 +168,11 @@ void testToolSchemaGeneration() { @Test void testSerializeBasicAgent() { Agent agent = Agent.builder() - .name("basic_agent") - .model("openai/gpt-4o") - .instructions("You are helpful.") - .maxTurns(5) - .build(); + .name("basic_agent") + .model("openai/gpt-4o") + .instructions("You are helpful.") + .maxTurns(5) + .build(); AgentConfigSerializer serializer = new AgentConfigSerializer(); Map config = serializer.serialize(agent); @@ -191,9 +185,7 @@ void testSerializeBasicAgent() { @Test void testSerializeExternalAgent() { - Agent external = Agent.builder() - .name("external_workflow") - .build(); + Agent external = Agent.builder().name("external_workflow").build(); AgentConfigSerializer serializer = new AgentConfigSerializer(); Map config = serializer.serialize(external); @@ -205,15 +197,16 @@ void testSerializeExternalAgent() { @Test void testSerializeMultiAgent() { - Agent researcher = Agent.builder().name("researcher").model("openai/gpt-4o").build(); + Agent researcher = + Agent.builder().name("researcher").model("openai/gpt-4o").build(); Agent writer = Agent.builder().name("writer").model("openai/gpt-4o").build(); Agent pipeline = Agent.builder() - .name("pipeline") - .model("openai/gpt-4o") - .agents(researcher, writer) - .strategy(Strategy.SEQUENTIAL) - .build(); + .name("pipeline") + .model("openai/gpt-4o") + .agents(researcher, writer) + .strategy(Strategy.SEQUENTIAL) + .build(); AgentConfigSerializer serializer = new AgentConfigSerializer(); Map config = serializer.serialize(pipeline); @@ -252,25 +245,22 @@ void testTerminationConditions() { @Test void testAgentConfigFromEnv() { AgentConfig config = AgentConfig.fromEnv(); - assertNotNull(config.getServerUrl()); assertTrue(config.getWorkerPollIntervalMs() > 0); assertTrue(config.getWorkerThreadCount() > 0); } @Test void testAgentConfigExplicit() { - AgentConfig config = new AgentConfig( - "http://myserver:8080/api", - "my-key", - "my-secret", - 200, - 10 - ); - - assertEquals("http://myserver:8080", config.getServerUrl()); - assertEquals("my-key", config.getAuthKey()); - assertEquals("my-secret", config.getAuthSecret()); + // AgentConfig now carries worker tuning only — server URL / auth moved to the client. + AgentConfig config = new AgentConfig(200, 10); assertEquals(200, config.getWorkerPollIntervalMs()); assertEquals(10, config.getWorkerThreadCount()); } + + @Test + void testConductorClientBuiltFromServerUrl() { + // Connection (server URL + auth) lives on the Conductor client, not AgentConfig. + var client = AgentRuntime.client("http://myserver:8080/api", "my-key", "my-secret"); + assertEquals("http://myserver:8080/api", client.getBasePath()); + } } diff --git a/sdk/java/src/test/java/ai/agentspan/ModelExecutionIdTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/ModelExecutionIdTest.java similarity index 77% rename from sdk/java/src/test/java/ai/agentspan/ModelExecutionIdTest.java rename to sdk/java/src/test/java/org/conductoross/conductor/ai/ModelExecutionIdTest.java index f9c167241..63fde53b2 100644 --- a/sdk/java/src/test/java/ai/agentspan/ModelExecutionIdTest.java +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/ModelExecutionIdTest.java @@ -1,20 +1,19 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan; +package org.conductoross.conductor.ai; -import ai.agentspan.enums.AgentStatus; -import ai.agentspan.enums.EventType; -import ai.agentspan.model.AgentEvent; -import ai.agentspan.model.AgentHandle; -import ai.agentspan.model.AgentResult; -import ai.agentspan.model.ToolContext; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.enums.EventType; +import org.conductoross.conductor.ai.model.AgentEvent; +import org.conductoross.conductor.ai.model.AgentResult; +import org.conductoross.conductor.ai.model.ToolContext; +import org.junit.jupiter.api.Test; /** * Verify that all public model classes expose {@code getExecutionId()} (not the @@ -25,8 +24,7 @@ class ModelExecutionIdTest { @Test void agentResult_getExecutionId() { AgentResult result = new AgentResult( - Map.of("result", "ok"), "exec-123", AgentStatus.COMPLETED, - List.of(), List.of(), null, null); + Map.of("result", "ok"), "exec-123", AgentStatus.COMPLETED, List.of(), List.of(), null, null); assertEquals("exec-123", result.getExecutionId()); } @@ -38,9 +36,8 @@ void agentResult_executionId_defaults_to_empty() { @Test void agentEvent_getExecutionId() { - AgentEvent event = new AgentEvent( - EventType.TOOL_CALL, null, "my_tool", Map.of(), null, null, - "exec-456", null, null); + AgentEvent event = + new AgentEvent(EventType.TOOL_CALL, null, "my_tool", Map.of(), null, null, "exec-456", null, null); assertEquals("exec-456", event.getExecutionId()); } diff --git a/sdk/java/src/test/java/ai/agentspan/SerializerTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/SerializerTest.java similarity index 57% rename from sdk/java/src/test/java/ai/agentspan/SerializerTest.java rename to sdk/java/src/test/java/org/conductoross/conductor/ai/SerializerTest.java index 3e133a764..5aadb10ff 100644 --- a/sdk/java/src/test/java/ai/agentspan/SerializerTest.java +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/SerializerTest.java @@ -1,36 +1,36 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan; - -import ai.agentspan.enums.OnFail; -import ai.agentspan.enums.Position; -import ai.agentspan.enums.Strategy; -import ai.agentspan.execution.CliConfig; -import ai.agentspan.gate.TextGate; -import ai.agentspan.guardrail.Guardrail; -import ai.agentspan.guardrail.LLMGuardrail; -import ai.agentspan.guardrail.RegexGuardrail; -import ai.agentspan.model.GuardrailResult; -import ai.agentspan.handoff.OnCondition; -import ai.agentspan.internal.AgentConfigSerializer; -import ai.agentspan.model.CredentialFile; -import ai.agentspan.model.GuardrailDef; -import ai.agentspan.model.ToolDef; -import ai.agentspan.openai.GPTAssistantAgent; -import ai.agentspan.skill.Skill; -import ai.agentspan.termination.StopMessageTermination; -import ai.agentspan.termination.TerminationResult; -import ai.agentspan.tools.HumanTool; -import ai.agentspan.tools.MediaTools; -import ai.agentspan.tools.WaitForMessageTool; -import org.junit.jupiter.api.Test; +package org.conductoross.conductor.ai; + +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.enums.OnFail; +import org.conductoross.conductor.ai.enums.Position; +import org.conductoross.conductor.ai.enums.Strategy; +import org.conductoross.conductor.ai.execution.CliConfig; +import org.conductoross.conductor.ai.gate.TextGate; +import org.conductoross.conductor.ai.guardrail.Guardrail; +import org.conductoross.conductor.ai.guardrail.LLMGuardrail; +import org.conductoross.conductor.ai.guardrail.RegexGuardrail; +import org.conductoross.conductor.ai.handoff.OnCondition; +import org.conductoross.conductor.ai.internal.AgentConfigSerializer; +import org.conductoross.conductor.ai.model.CredentialFile; +import org.conductoross.conductor.ai.model.GuardrailDef; +import org.conductoross.conductor.ai.model.GuardrailResult; +import org.conductoross.conductor.ai.model.ToolDef; +import org.conductoross.conductor.ai.openai.GPTAssistantAgent; +import org.conductoross.conductor.ai.plans.Context; +import org.conductoross.conductor.ai.termination.StopMessageTermination; +import org.conductoross.conductor.ai.termination.TerminationResult; +import org.conductoross.conductor.ai.tools.HumanTool; +import org.conductoross.conductor.ai.tools.MediaTools; +import org.conductoross.conductor.ai.tools.WaitForMessageTool; +import org.junit.jupiter.api.Test; /** * Unit tests for AgentConfigSerializer — new parity features. @@ -48,12 +48,12 @@ class SerializerTest { private ToolDef workerTool(String name) { return ToolDef.builder() - .name(name) - .description("A worker tool.") - .inputSchema(Map.of("type", "object", "properties", Map.of())) - .toolType("worker") - .func(input -> input) - .build(); + .name(name) + .description("A worker tool.") + .inputSchema(Map.of("type", "object", "properties", Map.of())) + .toolType("worker") + .func(input -> input) + .build(); } @SuppressWarnings("unchecked") @@ -61,13 +61,13 @@ private Map tool(Map out, String name) { List> tools = (List>) out.get("tools"); assertNotNull(tools, "serialized output has no 'tools' key"); return tools.stream() - .filter(t -> name.equals(t.get("name"))) - .findFirst() - .orElseGet(() -> { - fail("Tool '" + name + "' not found. Available: " - + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); - return null; - }); + .filter(t -> name.equals(t.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("Tool '" + name + "' not found. Available: " + + tools.stream().map(t -> (String) t.get("name")).collect(Collectors.toList())); + return null; + }); } @SuppressWarnings("unchecked") @@ -75,13 +75,13 @@ private Map guardrail(Map out, String name) { List> guards = (List>) out.get("guardrails"); assertNotNull(guards, "serialized output has no 'guardrails' key"); return guards.stream() - .filter(g -> name.equals(g.get("name"))) - .findFirst() - .orElseGet(() -> { - fail("Guardrail '" + name + "' not found. Available: " - + guards.stream().map(g -> (String) g.get("name")).collect(Collectors.toList())); - return null; - }); + .filter(g -> name.equals(g.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("Guardrail '" + name + "' not found. Available: " + + guards.stream().map(g -> (String) g.get("name")).collect(Collectors.toList())); + return null; + }); } // ── Stateful ───────────────────────────────────────────────────────── @@ -89,34 +89,93 @@ private Map guardrail(Map out, String name) { @Test void stateful_propagates_true_to_each_tool() { Agent agent = Agent.builder() - .name("stateful_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .stateful(true) - .tools(List.of(workerTool("tool_a"), workerTool("tool_b"))) - .build(); + .name("stateful_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .stateful(true) + .tools(List.of(workerTool("tool_a"), workerTool("tool_b"))) + .build(); Map out = ser.serialize(agent); - assertEquals(true, tool(out, "tool_a").get("stateful"), - "tool_a.stateful should be true when agent.stateful=true"); - assertEquals(true, tool(out, "tool_b").get("stateful"), - "tool_b.stateful should be true when agent.stateful=true"); + assertEquals( + true, tool(out, "tool_a").get("stateful"), "tool_a.stateful should be true when agent.stateful=true"); + assertEquals( + true, tool(out, "tool_b").get("stateful"), "tool_b.stateful should be true when agent.stateful=true"); } @Test void non_stateful_agent_does_not_set_tool_stateful() { Agent agent = Agent.builder() - .name("normal_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .tools(List.of(workerTool("tool_c"))) - .build(); + .name("normal_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .tools(List.of(workerTool("tool_c"))) + .build(); + + Map out = ser.serialize(agent); + + assertNull(tool(out, "tool_c").get("stateful"), "tool.stateful should be null/absent for a non-stateful agent"); + } + + // ── CLI config ────────────────────────────────────────────────────────── + + @Test + @SuppressWarnings("unchecked") + void cli_config_injects_run_command_worker_tool() { + Agent agent = Agent.builder() + .name("ops") + .model("openai/gpt-4o-mini") + .instructions("test") + .cliConfig(CliConfig.builder() + .allowedCommands(List.of("git", "gh")) + .timeout(60) + .build()) + .build(); + + Map out = ser.serialize(agent); + + // Agent-level cliConfig block still serialized. + Map cliMap = (Map) out.get("cliConfig"); + assertNotNull(cliMap, "cliConfig block must be serialized"); + assertEquals(List.of("git", "gh"), cliMap.get("allowedCommands")); + + // The {name}_run_command worker tool must be injected so the LLM can call it. + Map rc = tool(out, "ops_run_command"); + assertEquals("worker", rc.get("toolType")); + // Allowed commands are advertised sorted (parity with Python/TS/C#). + assertTrue( + ((String) rc.get("description")).contains("gh, git"), + "description should advertise the allowed commands: " + rc.get("description")); + Map schema = (Map) rc.get("inputSchema"); + Map props = (Map) schema.get("properties"); + assertTrue( + props.containsKey("command") + && props.containsKey("args") + && props.containsKey("cwd") + && props.containsKey("shell"), + "input schema must expose command/args/cwd/shell"); + assertEquals(List.of("command"), schema.get("required")); + } + + @Test + @SuppressWarnings("unchecked") + void disabled_cli_config_does_not_inject_run_command_tool() { + Agent agent = Agent.builder() + .name("ops2") + .model("openai/gpt-4o-mini") + .instructions("test") + .cliConfig(CliConfig.builder() + .enabled(false) + .allowedCommands(List.of("git")) + .build()) + .build(); Map out = ser.serialize(agent); - assertNull(tool(out, "tool_c").get("stateful"), - "tool.stateful should be null/absent for a non-stateful agent"); + List> tools = (List>) out.get("tools"); + boolean hasRunCommand = tools != null && tools.stream().anyMatch(t -> "ops2_run_command".equals(t.get("name"))); + assertFalse(hasRunCommand, "disabled cliConfig must not inject a run_command tool"); } // ── baseUrl ─────────────────────────────────────────────────────────── @@ -124,25 +183,24 @@ void non_stateful_agent_does_not_set_tool_stateful() { @Test void base_url_serialized() { Agent agent = Agent.builder() - .name("baseurl_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .baseUrl("http://proxy.internal/v1") - .build(); + .name("baseurl_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .baseUrl("http://proxy.internal/v1") + .build(); Map out = ser.serialize(agent); - assertEquals("http://proxy.internal/v1", out.get("baseUrl"), - "baseUrl should appear in serialized output"); + assertEquals("http://proxy.internal/v1", out.get("baseUrl"), "baseUrl should appear in serialized output"); } @Test void missing_base_url_not_serialized() { Agent agent = Agent.builder() - .name("no_baseurl_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .build(); + .name("no_baseurl_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .build(); Map out = ser.serialize(agent); @@ -154,11 +212,11 @@ void missing_base_url_not_serialized() { @Test void text_gate_serialized_with_all_fields() { Agent agent = Agent.builder() - .name("gate_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .gate(new TextGate("STOP", false)) - .build(); + .name("gate_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .gate(new TextGate("STOP", false)) + .build(); Map out = ser.serialize(agent); @@ -173,18 +231,17 @@ void text_gate_serialized_with_all_fields() { @Test void text_gate_case_sensitive_default() { Agent agent = Agent.builder() - .name("gate_cs_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .gate(new TextGate("DONE")) - .build(); + .name("gate_cs_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .gate(new TextGate("DONE")) + .build(); Map out = ser.serialize(agent); @SuppressWarnings("unchecked") Map gate = (Map) out.get("gate"); - assertEquals(true, gate.get("caseSensitive"), - "Default TextGate should be case-sensitive"); + assertEquals(true, gate.get("caseSensitive"), "Default TextGate should be case-sensitive"); } // ── before/after agent callbacks ────────────────────────────────────── @@ -193,11 +250,11 @@ void text_gate_case_sensitive_default() { @SuppressWarnings("unchecked") void before_agent_callback_serialized() { Agent agent = Agent.builder() - .name("before_cb_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .beforeAgentCallback(ctx -> ctx) - .build(); + .name("before_cb_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .beforeAgentCallback(ctx -> ctx) + .build(); Map out = ser.serialize(agent); @@ -212,11 +269,11 @@ void before_agent_callback_serialized() { @SuppressWarnings("unchecked") void after_agent_callback_serialized() { Agent agent = Agent.builder() - .name("after_cb_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .afterAgentCallback(ctx -> ctx) - .build(); + .name("after_cb_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .afterAgentCallback(ctx -> ctx) + .build(); Map out = ser.serialize(agent); @@ -231,12 +288,12 @@ void after_agent_callback_serialized() { @SuppressWarnings("unchecked") void both_callbacks_produce_two_entries() { Agent agent = Agent.builder() - .name("both_cb_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .beforeAgentCallback(ctx -> ctx) - .afterAgentCallback(ctx -> ctx) - .build(); + .name("both_cb_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .beforeAgentCallback(ctx -> ctx) + .afterAgentCallback(ctx -> ctx) + .build(); Map out = ser.serialize(agent); @@ -244,9 +301,8 @@ void both_callbacks_produce_two_entries() { assertNotNull(callbacks); assertEquals(2, callbacks.size(), "Both before and after agent callbacks must produce 2 entries"); - List positions = callbacks.stream() - .map(cb -> (String) cb.get("position")) - .collect(Collectors.toList()); + List positions = + callbacks.stream().map(cb -> (String) cb.get("position")).collect(Collectors.toList()); assertTrue(positions.contains("before_agent")); assertTrue(positions.contains("after_agent")); } @@ -254,10 +310,10 @@ void both_callbacks_produce_two_entries() { @Test void no_callbacks_absent_from_output() { Agent agent = Agent.builder() - .name("no_cb_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .build(); + .name("no_cb_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .build(); Map out = ser.serialize(agent); @@ -270,11 +326,11 @@ void no_callbacks_absent_from_output() { @SuppressWarnings("unchecked") void stop_message_termination_serialized() { Agent agent = Agent.builder() - .name("stop_msg_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .termination(StopMessageTermination.of("DONE")) - .build(); + .name("stop_msg_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .termination(StopMessageTermination.of("DONE")) + .build(); Map out = ser.serialize(agent); @@ -313,18 +369,18 @@ void termination_result_continue() { @Test void regex_guardrail_type_and_patterns_at_top_level() { GuardrailDef guard = RegexGuardrail.builder() - .name("pii_guard") - .patterns("[\\w.+-]+@[\\w-]+\\.[\\w.-]+") - .position(Position.OUTPUT) - .onFail(OnFail.RETRY) - .build(); + .name("pii_guard") + .patterns("[\\w.+-]+@[\\w-]+\\.[\\w.-]+") + .position(Position.OUTPUT) + .onFail(OnFail.RETRY) + .build(); Agent agent = Agent.builder() - .name("regex_guard_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .guardrails(List.of(guard)) - .build(); + .name("regex_guard_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .guardrails(List.of(guard)) + .build(); Map out = ser.serialize(agent); Map g = guardrail(out, "pii_guard"); @@ -339,37 +395,36 @@ void regex_guardrail_type_and_patterns_at_top_level() { @Test void regex_guardrail_block_mode_default() { GuardrailDef guard = RegexGuardrail.builder() - .name("block_guard") - .patterns("bad_word") - .build(); + .name("block_guard") + .patterns("bad_word") + .build(); Agent agent = Agent.builder() - .name("block_guard_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .guardrails(List.of(guard)) - .build(); + .name("block_guard_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .guardrails(List.of(guard)) + .build(); Map out = ser.serialize(agent); Map g = guardrail(out, "block_guard"); - assertEquals("block", g.get("mode"), - "Default mode should be 'block'"); + assertEquals("block", g.get("mode"), "Default mode should be 'block'"); } @Test void regex_guardrail_allow_mode() { GuardrailDef guard = RegexGuardrail.builder() - .name("allow_guard") - .patterns("^\\s*[\\{\\[]") - .mode("allow") - .build(); + .name("allow_guard") + .patterns("^\\s*[\\{\\[]") + .mode("allow") + .build(); Agent agent = Agent.builder() - .name("allow_guard_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .guardrails(List.of(guard)) - .build(); + .name("allow_guard_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .guardrails(List.of(guard)) + .build(); Map out = ser.serialize(agent); Map g = guardrail(out, "allow_guard"); @@ -378,9 +433,9 @@ void regex_guardrail_allow_mode() { @Test void regex_guardrail_requires_patterns() { - assertThrows(IllegalArgumentException.class, () -> - RegexGuardrail.builder().name("empty").build() - ); + assertThrows( + IllegalArgumentException.class, + () -> RegexGuardrail.builder().name("empty").build()); } // ── LLMGuardrail ────────────────────────────────────────────────────── @@ -388,40 +443,39 @@ void regex_guardrail_requires_patterns() { @Test void llm_guardrail_type_model_policy_at_top_level() { GuardrailDef guard = LLMGuardrail.builder() - .name("safety_guard") - .model("openai/gpt-4o-mini") - .policy("Reject harmful content.") - .position(Position.OUTPUT) - .onFail(OnFail.RETRY) - .build(); + .name("safety_guard") + .model("openai/gpt-4o-mini") + .policy("Reject harmful content.") + .position(Position.OUTPUT) + .onFail(OnFail.RETRY) + .build(); Agent agent = Agent.builder() - .name("llm_guard_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .guardrails(List.of(guard)) - .build(); + .name("llm_guard_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .guardrails(List.of(guard)) + .build(); Map out = ser.serialize(agent); Map g = guardrail(out, "safety_guard"); assertEquals("llm", g.get("guardrailType")); // model and policy are inlined at top level, NOT nested under config - assertEquals("openai/gpt-4o-mini", g.get("model"), - "model should be at top level of guardrail map"); - assertEquals("Reject harmful content.", g.get("policy"), - "policy should be at top level of guardrail map"); + assertEquals("openai/gpt-4o-mini", g.get("model"), "model should be at top level of guardrail map"); + assertEquals("Reject harmful content.", g.get("policy"), "policy should be at top level of guardrail map"); assertNull(g.get("config"), "there should be no nested 'config' key"); } @Test void llm_guardrail_requires_model_and_policy() { - assertThrows(IllegalArgumentException.class, () -> - LLMGuardrail.builder().name("no_model").policy("test").build() - ); - assertThrows(IllegalArgumentException.class, () -> - LLMGuardrail.builder().name("no_policy").model("openai/gpt-4o-mini").build() - ); + assertThrows( + IllegalArgumentException.class, + () -> LLMGuardrail.builder().name("no_model").policy("test").build()); + assertThrows(IllegalArgumentException.class, () -> LLMGuardrail.builder() + .name("no_policy") + .model("openai/gpt-4o-mini") + .build()); } // ── OnCondition handoff ─────────────────────────────────────────────── @@ -429,31 +483,34 @@ void llm_guardrail_requires_model_and_policy() { @Test @SuppressWarnings("unchecked") void on_condition_handoff_serialized_with_target() { - Agent supervisor = Agent.builder().name("supervisor").model("openai/gpt-4o-mini").build(); + Agent supervisor = + Agent.builder().name("supervisor").model("openai/gpt-4o-mini").build(); Agent worker = Agent.builder() - .name("worker") - .model("openai/gpt-4o-mini") - .instructions("test") - .handoffs(List.of(new OnCondition("supervisor", - ctx -> Boolean.TRUE.equals(ctx.get("escalate"))))) - .build(); + .name("worker") + .model("openai/gpt-4o-mini") + .instructions("test") + .handoffs(List.of(new OnCondition("supervisor", ctx -> Boolean.TRUE.equals(ctx.get("escalate"))))) + .build(); Agent team = Agent.builder() - .name("team") - .model("openai/gpt-4o-mini") - .instructions("test") - .agents(supervisor, worker) - .strategy(Strategy.HANDOFF) - .build(); + .name("team") + .model("openai/gpt-4o-mini") + .instructions("test") + .agents(supervisor, worker) + .strategy(Strategy.HANDOFF) + .build(); Map out = ser.serialize(team); List> agents = (List>) out.get("agents"); assertNotNull(agents); Map workerOut = agents.stream() - .filter(a -> "worker".equals(a.get("name"))) - .findFirst() - .orElseGet(() -> { fail("worker not found"); return null; }); + .filter(a -> "worker".equals(a.get("name"))) + .findFirst() + .orElseGet(() -> { + fail("worker not found"); + return null; + }); List> handoffs = (List>) workerOut.get("handoffs"); assertNotNull(handoffs, "handoffs should be serialized"); @@ -461,66 +518,41 @@ void on_condition_handoff_serialized_with_target() { assertEquals("supervisor", handoffs.get(0).get("target")); } - // ── UserProxyAgent ──────────────────────────────────────────────────── - - @Test - @SuppressWarnings("unchecked") - void user_proxy_agent_sets_metadata() { - Agent proxy = UserProxyAgent.create("human_user", "ALWAYS", "Continue.", "openai/gpt-4o-mini"); - Map out = ser.serialize(proxy); - - Map metadata = (Map) out.get("metadata"); - assertNotNull(metadata, "UserProxyAgent must set metadata"); - assertEquals("user_proxy", metadata.get("_agent_type")); - assertEquals("ALWAYS", metadata.get("_human_input_mode")); - } - - @Test - void user_proxy_agent_rejects_invalid_mode() { - assertThrows(IllegalArgumentException.class, () -> - UserProxyAgent.create("u", "INVALID_MODE", "ok", "openai/gpt-4o-mini") - ); - } - // ── MediaTools ──────────────────────────────────────────────────────── @Test void image_tool_has_generate_image_type() { ToolDef img = MediaTools.imageTool("my_img", "Generate image", "openai", "dall-e-3"); - assertEquals("generate_image", img.getToolType(), - "imageTool must have toolType='generate_image'"); + assertEquals("generate_image", img.getToolType(), "imageTool must have toolType='generate_image'"); } @Test void audio_tool_has_generate_audio_type() { ToolDef aud = MediaTools.audioTool("my_audio", "Speak text", "openai", "tts-1"); - assertEquals("generate_audio", aud.getToolType(), - "audioTool must have toolType='generate_audio'"); + assertEquals("generate_audio", aud.getToolType(), "audioTool must have toolType='generate_audio'"); } @Test void video_tool_has_generate_video_type() { ToolDef vid = MediaTools.videoTool("my_video", "Make video", "openai", "sora-2"); - assertEquals("generate_video", vid.getToolType(), - "videoTool must have toolType='generate_video'"); + assertEquals("generate_video", vid.getToolType(), "videoTool must have toolType='generate_video'"); } @Test void pdf_tool_has_generate_pdf_type() { ToolDef pdf = MediaTools.pdfTool(); - assertEquals("generate_pdf", pdf.getToolType(), - "pdfTool must have toolType='generate_pdf'"); + assertEquals("generate_pdf", pdf.getToolType(), "pdfTool must have toolType='generate_pdf'"); } @Test void media_tools_serialized_in_agent() { ToolDef img = MediaTools.imageTool("img_tool", "image", "openai", "dall-e-3"); Agent agent = Agent.builder() - .name("media_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .tools(List.of(img)) - .build(); + .name("media_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .tools(List.of(img)) + .build(); Map out = ser.serialize(agent); assertEquals("generate_image", tool(out, "img_tool").get("toolType")); @@ -538,11 +570,11 @@ void wait_for_message_tool_type() { void wait_for_message_tool_serialized_in_agent() { ToolDef wait = WaitForMessageTool.create("wait_tool", "Wait for messages"); Agent agent = Agent.builder() - .name("wait_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .tools(List.of(wait)) - .build(); + .name("wait_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .tools(List.of(wait)) + .build(); Map out = ser.serialize(agent); assertEquals("pull_workflow_messages", tool(out, "wait_tool").get("toolType")); @@ -560,59 +592,25 @@ void human_tool_type() { void human_tool_serialized_in_agent() { ToolDef human = HumanTool.create("human_tool", "Ask human"); Agent agent = Agent.builder() - .name("human_agent") - .model("openai/gpt-4o-mini") - .instructions("test") - .tools(List.of(human)) - .build(); + .name("human_agent") + .model("openai/gpt-4o-mini") + .instructions("test") + .tools(List.of(human)) + .build(); Map out = ser.serialize(agent); assertEquals("human", tool(out, "human_tool").get("toolType")); } - // ── ClaudeCode ──────────────────────────────────────────────────────── - - @Test - void claude_code_model_string_format() { - ClaudeCode cc = new ClaudeCode("opus"); - assertTrue(cc.toModelString().startsWith("claude-code/"), - "toModelString() must start with 'claude-code/'"); - assertTrue(cc.toModelString().contains("opus")); - } - - @Test - void claude_code_permission_modes() { - for (ClaudeCode.PermissionMode mode : ClaudeCode.PermissionMode.values()) { - ClaudeCode cc = new ClaudeCode("sonnet", mode); - assertNotNull(cc.toModelString()); - assertTrue(cc.toModelString().startsWith("claude-code/")); - } - } - - @Test - void claude_code_model_in_agent_serialized() { - ClaudeCode cc = new ClaudeCode("sonnet"); - Agent agent = Agent.builder() - .name("cc_agent") - .model(cc.toModelString()) - .instructions("test") - .build(); - - Map out = ser.serialize(agent); - String model = (String) out.get("model"); - assertTrue(model.startsWith("claude-code/"), - "model in serialized output must start with 'claude-code/'"); - } - // ── GPTAssistantAgent ───────────────────────────────────────────────── @Test @SuppressWarnings("unchecked") void gpt_assistant_sets_agent_type_metadata() { Agent agent = GPTAssistantAgent.create("my_assistant") - .model("gpt-4o") - .instructions("You are helpful.") - .build(); + .model("gpt-4o") + .instructions("You are helpful.") + .build(); Map out = ser.serialize(agent); @@ -625,8 +623,8 @@ void gpt_assistant_sets_agent_type_metadata() { @SuppressWarnings("unchecked") void gpt_assistant_with_existing_id_sets_assistant_id_in_metadata() { Agent agent = GPTAssistantAgent.create("existing_assistant") - .assistantId("asst_abc123") - .build(); + .assistantId("asst_abc123") + .build(); Map out = ser.serialize(agent); @@ -638,18 +636,14 @@ void gpt_assistant_with_existing_id_sets_assistant_id_in_metadata() { @Test @SuppressWarnings("unchecked") void gpt_assistant_has_call_tool() { - Agent agent = GPTAssistantAgent.create("tool_assistant") - .model("gpt-4o") - .build(); + Agent agent = GPTAssistantAgent.create("tool_assistant").model("gpt-4o").build(); Map out = ser.serialize(agent); List> tools = (List>) out.get("tools"); assertNotNull(tools); - boolean hasCallTool = tools.stream() - .anyMatch(t -> ((String) t.get("name")).endsWith("_assistant_call")); - assertTrue(hasCallTool, - "GPTAssistantAgent must have a '{name}_assistant_call' tool"); + boolean hasCallTool = tools.stream().anyMatch(t -> ((String) t.get("name")).endsWith("_assistant_call")); + assertTrue(hasCallTool, "GPTAssistantAgent must have a '{name}_assistant_call' tool"); } @Test @@ -661,8 +655,10 @@ void gpt_assistant_normalizes_model_prefix() { Map out2 = ser.serialize(withoutPrefix); assertEquals("openai/gpt-4o", out1.get("model")); - assertEquals("openai/gpt-4o", out2.get("model"), - "model without 'openai/' prefix should be normalized to 'openai/gpt-4o'"); + assertEquals( + "openai/gpt-4o", + out2.get("model"), + "model without 'openai/' prefix should be normalized to 'openai/gpt-4o'"); } // ── Skill agent ─────────────────────────────────────────────────────── @@ -670,19 +666,17 @@ void gpt_assistant_normalizes_model_prefix() { @Test void skill_agent_uses_framework_fast_path() { Agent skillAgent = Agent.builder() - .name("my_skill") - .model("openai/gpt-4o-mini") - .framework("skill") - .frameworkConfig(Map.of("skillMd", "# My Skill\nDo things.")) - .build(); + .name("my_skill") + .model("openai/gpt-4o-mini") + .framework("skill") + .frameworkConfig(Map.of("skillMd", "# My Skill\nDo things.")) + .build(); Map out = ser.serialize(skillAgent); - assertEquals("skill", out.get("_framework"), - "Skill agent must serialize _framework='skill'"); + assertEquals("skill", out.get("_framework"), "Skill agent must serialize _framework='skill'"); assertEquals("my_skill", out.get("name")); - assertNotNull(out.get("skillMd"), - "frameworkConfig contents should be inlined into the output"); + assertNotNull(out.get("skillMd"), "frameworkConfig contents should be inlined into the output"); } // ── CredentialFile ──────────────────────────────────────────────────── @@ -703,31 +697,34 @@ void credential_file_with_content() { assertEquals("MY_KEY", filled.getEnvVar()); } - // --- AgentConfig URL normalization --- + // --- Conductor client base-path normalization (server URL now lives on the client) --- @Test - void agentConfig_default_url_has_no_api_suffix() { - AgentConfig cfg = AgentConfig.fromEnv(); - assertFalse(cfg.getServerUrl().endsWith("/api"), - "Default URL must not end with /api — HttpApi already prepends /api/ to every path"); + void client_default_base_path_ends_with_api() { + assertEquals( + "http://localhost:6767/api", + AgentRuntime.client("http://localhost:6767").getBasePath()); } @Test - void agentConfig_strips_trailing_api_from_user_supplied_url() { - AgentConfig cfg = new AgentConfig("http://localhost:6767/api", null, null, 100, 1); - assertEquals("http://localhost:6767", cfg.getServerUrl()); + void client_strips_trailing_api_then_re_appends() { + assertEquals( + "http://localhost:6767/api", + AgentRuntime.client("http://localhost:6767/api").getBasePath()); } @Test - void agentConfig_strips_trailing_slash_and_api() { - AgentConfig cfg = new AgentConfig("http://localhost:6767/api/", null, null, 100, 1); - assertEquals("http://localhost:6767", cfg.getServerUrl()); + void client_strips_trailing_slash_and_api() { + assertEquals( + "http://localhost:6767/api", + AgentRuntime.client("http://localhost:6767/api/").getBasePath()); } @Test - void agentConfig_plain_url_unchanged() { - AgentConfig cfg = new AgentConfig("http://localhost:6767", null, null, 100, 1); - assertEquals("http://localhost:6767", cfg.getServerUrl()); + void client_plain_url_gets_api_suffix() { + assertEquals( + "http://localhost:6767/api", + AgentRuntime.client("http://localhost:6767").getBasePath()); } // --- CliConfig serialization --- @@ -766,10 +763,11 @@ void cliConfig_absent_when_not_set() { @Test @SuppressWarnings("unchecked") void guardrail_custom_serialized_as_custom_type() { - GuardrailDef g = Guardrail.of("no_bad_words", content -> - GuardrailResult.pass()).build(); + GuardrailDef g = + Guardrail.of("no_bad_words", content -> GuardrailResult.pass()).build(); Agent agent = Agent.builder() - .name("a").model("openai/gpt-4o") + .name("a") + .model("openai/gpt-4o") .guardrails(List.of(g)) .build(); Map out = ser.serialize(agent); @@ -781,9 +779,11 @@ void guardrail_custom_serialized_as_custom_type() { @Test @SuppressWarnings("unchecked") void guardrail_external_serialized_as_external_type() { - GuardrailDef g = Guardrail.external("corporate_safety").position(Position.INPUT).build(); + GuardrailDef g = + Guardrail.external("corporate_safety").position(Position.INPUT).build(); Agent agent = Agent.builder() - .name("a").model("openai/gpt-4o") + .name("a") + .model("openai/gpt-4o") .guardrails(List.of(g)) .build(); Map out = ser.serialize(agent); @@ -806,7 +806,8 @@ void tool_retry_policy_serialized_when_non_default() { .retryPolicy("exponential_backoff") .build(); Agent agent = Agent.builder() - .name("a").model("openai/gpt-4o") + .name("a") + .model("openai/gpt-4o") .tools(List.of(t)) .build(); Map out = ser.serialize(agent); @@ -826,7 +827,8 @@ void tool_retry_policy_omitted_when_default() { .inputSchema(Map.of("type", "object", "properties", Map.of())) .build(); Agent agent = Agent.builder() - .name("a").model("openai/gpt-4o") + .name("a") + .model("openai/gpt-4o") .tools(List.of(t)) .build(); Map out = ser.serialize(agent); @@ -845,20 +847,22 @@ void planner_context_emitted_with_text_and_url_entries() { // Mirrors the Python + TS serializer tests. The wire shape MUST be // byte-equal across SDKs so the server compiler sees the same // payload regardless of language. - Agent planner = Agent.builder().name("planner_sub").model("openai/gpt-4o-mini").build(); + Agent planner = + Agent.builder().name("planner_sub").model("openai/gpt-4o-mini").build(); ToolDef stub = ToolDef.builder() .name("stub") .description("stub") .inputSchema(Map.of("type", "object", "properties", Map.of())) .build(); Agent harness = Agent.builder() - .name("h").model("openai/gpt-4o-mini") + .name("h") + .model("openai/gpt-4o-mini") .strategy(Strategy.PLAN_EXECUTE) .planner(planner) .tools(List.of(stub)) .plannerContext(List.of( - ai.agentspan.plans.Context.text("inline rule"), - ai.agentspan.plans.Context.builder() + Context.text("inline rule"), + Context.builder() .url("https://confluence.example.com/onboarding") .header("Authorization", "Bearer ${CONFLUENCE_TOKEN}") .required(false) @@ -872,9 +876,7 @@ void planner_context_emitted_with_text_and_url_entries() { Map urlEntry = ctx.get(1); assertEquals("https://confluence.example.com/onboarding", urlEntry.get("url")); // Credential placeholder MUST pass through verbatim — server escapes. - assertEquals( - Map.of("Authorization", "Bearer ${CONFLUENCE_TOKEN}"), - urlEntry.get("headers")); + assertEquals(Map.of("Authorization", "Bearer ${CONFLUENCE_TOKEN}"), urlEntry.get("headers")); assertEquals(false, urlEntry.get("required")); assertEquals(8192, urlEntry.get("maxBytes")); } @@ -883,14 +885,16 @@ void planner_context_emitted_with_text_and_url_entries() { void planner_context_omitted_when_unset() { // Counterfactual: without plannerContext the field MUST NOT appear // on the wire. Pairs with the positive test — pins the gating. - Agent planner = Agent.builder().name("planner_sub").model("openai/gpt-4o-mini").build(); + Agent planner = + Agent.builder().name("planner_sub").model("openai/gpt-4o-mini").build(); ToolDef stub = ToolDef.builder() .name("stub") .description("stub") .inputSchema(Map.of("type", "object", "properties", Map.of())) .build(); Agent harness = Agent.builder() - .name("h").model("openai/gpt-4o-mini") + .name("h") + .model("openai/gpt-4o-mini") .strategy(Strategy.PLAN_EXECUTE) .planner(planner) .tools(List.of(stub)) @@ -904,14 +908,60 @@ void planner_context_rejected_on_non_plan_execute_strategy() { // Same guard shape as planner=/fallback= — setting plannerContext // on anything other than PLAN_EXECUTE is a silent bug. Agent sub = Agent.builder().name("sub").model("openai/gpt-4o-mini").build(); - IllegalArgumentException e = assertThrows( - IllegalArgumentException.class, - () -> Agent.builder() - .name("h").model("openai/gpt-4o-mini") - .strategy(Strategy.HANDOFF) - .agents(List.of(sub)) - .plannerContext("rule") - .build()); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> Agent.builder() + .name("h") + .model("openai/gpt-4o-mini") + .strategy(Strategy.HANDOFF) + .agents(List.of(sub)) + .plannerContext("rule") + .build()); assertTrue(e.getMessage().contains("PLAN_EXECUTE")); } + + // ── Parity fields: reasoningEffort, maskedFields, contextWindowBudget, memory ── + + @Test + @SuppressWarnings("unchecked") + void parity_fields_serialized() { + org.conductoross.conductor.ai.model.ConversationMemory memory = + new org.conductoross.conductor.ai.model.ConversationMemory(20) + .addSystem("You are concise.") + .addUser("hi"); + + Agent agent = Agent.builder() + .name("parity_agent") + .model("openai/o3-mini") + .instructions("test") + .reasoningEffort("high") + .maskedFields("ssn", "card_number") + .contextWindowBudget(8000) + .memory(memory) + .build(); + + Map out = ser.serialize(agent); + + assertEquals("high", out.get("reasoningEffort"), "reasoningEffort must serialize"); + assertEquals(List.of("ssn", "card_number"), out.get("maskedFields"), "maskedFields must serialize"); + assertEquals(8000, out.get("contextWindowBudget"), "contextWindowBudget must serialize"); + + Map mem = (Map) out.get("memory"); + assertNotNull(mem, "memory must serialize as a map"); + assertEquals(20, mem.get("maxMessages"), "memory.maxMessages must serialize"); + List> msgs = (List>) mem.get("messages"); + assertEquals(2, msgs.size(), "memory.messages must carry both messages"); + assertEquals("system", msgs.get(0).get("role")); + assertEquals("You are concise.", msgs.get(0).get("message")); + assertEquals("user", msgs.get(1).get("role")); + } + + @Test + void parity_fields_absent_when_unset() { + Agent agent = + Agent.builder().name("plain_agent").model("openai/gpt-4o-mini").build(); + Map out = ser.serialize(agent); + assertFalse(out.containsKey("reasoningEffort"), "reasoningEffort omitted when unset"); + assertFalse(out.containsKey("maskedFields"), "maskedFields omitted when unset"); + assertFalse(out.containsKey("contextWindowBudget"), "contextWindowBudget omitted when unset"); + assertFalse(out.containsKey("memory"), "memory omitted when unset"); + } } diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/ToolContextCredentialsTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/ToolContextCredentialsTest.java new file mode 100644 index 000000000..5c2811d16 --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/ToolContextCredentialsTest.java @@ -0,0 +1,157 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.conductoross.conductor.ai.exceptions.CredentialNotFoundException; +import org.conductoross.conductor.ai.internal.CredentialContext; +import org.conductoross.conductor.ai.model.ToolContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for per-call credential injection: the public {@link ToolContext} read API + * and the internal {@link CredentialContext} transport that carries resolved secrets from + * {@code WorkerManager} to {@code ToolRegistry} on the worker thread. + * + *

    Covers: reading via {@link ToolContext#getCredential(String)} / + * {@link ToolContext#getCredentialOrNull(String)}; missing name → typed exception; the + * credential snapshot is immutable and remains readable from a thread the tool spawns even + * after the worker thread clears the transport; and per-thread isolation of the transport. + */ +class ToolContextCredentialsTest { + + @AfterEach + void reset() { + CredentialContext.clear(); + } + + // ── ToolContext read API ──────────────────────────────────────────────── + + @Test + void getCredential_returns_value() { + ToolContext ctx = new ToolContext(null, null, null, null, Map.of("OPENAI_API_KEY", "sk-test-123")); + assertEquals("sk-test-123", ctx.getCredential("OPENAI_API_KEY")); + assertEquals("sk-test-123", ctx.getCredentialOrNull("OPENAI_API_KEY")); + } + + @Test + void getCredential_missing_throws_typed_exception() { + ToolContext ctx = new ToolContext(null, null, null, null, Map.of("KNOWN", "v")); + assertThrows(CredentialNotFoundException.class, () -> ctx.getCredential("UNKNOWN")); + } + + @Test + void getCredentialOrNull_missing_returns_null() { + ToolContext ctx = new ToolContext(null, null, null); + assertNull(ctx.getCredentialOrNull("ANY")); + assertThrows(CredentialNotFoundException.class, () -> ctx.getCredential("ANY")); + } + + @Test + void getCredentials_view_is_immutable() { + ToolContext ctx = new ToolContext(null, null, null, null, Map.of("A", "1", "B", "2")); + Map view = ctx.getCredentials(); + assertEquals(2, view.size()); + assertThrows(UnsupportedOperationException.class, () -> view.put("C", "3")); + } + + @Test + void snapshot_is_decoupled_from_source_map() { + java.util.Map src = new java.util.HashMap<>(); + src.put("A", "1"); + ToolContext ctx = new ToolContext(null, null, null, null, src); + src.put("B", "2"); // mutate source after construction + src.clear(); + assertEquals("1", ctx.getCredential("A")); + assertNull(ctx.getCredentialOrNull("B")); + } + + // ── Multi-threading ─────────────────────────────────────────────────────── + + /** + * The credential snapshot lives on the ToolContext object, not a thread-local, so a + * thread the tool spawns can read it — and it stays valid even after the worker thread + * that built the context clears the transport (mirrors the WorkerManager finally block). + */ + @Test + void credentials_readable_from_child_thread_after_transport_cleared() throws Exception { + CredentialContext.set(Map.of("TOKEN", "secret-xyz")); + // ToolRegistry snapshots the transport into the ToolContext on the worker thread. + ToolContext ctx = new ToolContext(null, null, null, null, CredentialContext.current()); + CredentialContext.clear(); // worker thread's finally runs + + AtomicReference seen = new AtomicReference<>(); + AtomicReference transportSeen = new AtomicReference<>("UNSET"); + Thread child = new Thread(() -> { + seen.set(ctx.getCredentialOrNull("TOKEN")); // from the context object → still valid + transportSeen.set(CredentialContext.current().get("TOKEN")); // transport is thread-local → not visible + }); + child.start(); + child.join(2000); + + assertEquals("secret-xyz", seen.get(), "child thread must read the credential off the ToolContext"); + assertNull(transportSeen.get(), "the thread-local transport must NOT leak into other threads"); + } + + /** Concurrent worker threads see independent transport contexts. */ + @Test + void transport_is_isolated_per_thread() throws Exception { + AtomicReference a = new AtomicReference<>(); + AtomicReference b = new AtomicReference<>(); + CountDownLatch bothSet = new CountDownLatch(2); + CountDownLatch read = new CountDownLatch(2); + + Thread ta = new Thread(() -> { + CredentialContext.set(Map.of("KEY", "value-A")); + bothSet.countDown(); + try { + bothSet.await(2, TimeUnit.SECONDS); // ensure both have set before either reads + } catch (InterruptedException ignored) { + } + a.set(CredentialContext.current().get("KEY")); + read.countDown(); + CredentialContext.clear(); + }); + Thread tb = new Thread(() -> { + CredentialContext.set(Map.of("KEY", "value-B")); + bothSet.countDown(); + try { + bothSet.await(2, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + } + b.set(CredentialContext.current().get("KEY")); + read.countDown(); + CredentialContext.clear(); + }); + ta.start(); + tb.start(); + assertTrue(read.await(3, TimeUnit.SECONDS), "threads did not finish in time"); + + assertEquals("value-A", a.get()); + assertEquals("value-B", b.get()); + } + + // ── Transport edge cases ──────────────────────────────────────────────── + + @Test + void transport_empty_map_clears() { + CredentialContext.set(Map.of("X", "y")); + CredentialContext.set(Map.of()); + assertTrue(CredentialContext.current().isEmpty()); + } + + @Test + void transport_null_clears() { + CredentialContext.set(Map.of("X", "y")); + CredentialContext.set(null); + assertTrue(CredentialContext.current().isEmpty()); + } +} diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/exceptions/ExceptionsTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/exceptions/ExceptionsTest.java new file mode 100644 index 000000000..4b2f66eaa --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/exceptions/ExceptionsTest.java @@ -0,0 +1,57 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.exceptions; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +/** Pure unit tests for the SDK exception hierarchy (status/message/typed fields). */ +class ExceptionsTest { + + @Test + void agentApiCarriesStatusAndBody() { + AgentAPIException e = new AgentAPIException(500, "boom"); + assertEquals(500, e.getStatusCode()); + assertEquals("boom", e.getResponseBody()); + assertInstanceOf(AgentspanException.class, e); + } + + @Test + void notFoundIsApiExceptionWith404() { + AgentNotFoundException e = new AgentNotFoundException(404, "missing"); + assertEquals(404, e.getStatusCode()); + assertInstanceOf(AgentAPIException.class, e); + assertInstanceOf(AgentspanException.class, e); + } + + @Test + void credentialNotFoundListsMissingNames() { + CredentialNotFoundException e = new CredentialNotFoundException(List.of("A", "B")); + assertEquals(List.of("A", "B"), e.getMissingNames()); + CredentialNotFoundException single = new CredentialNotFoundException("ONLY"); + assertTrue(single.getMissingNames().contains("ONLY")); + } + + @Test + void credentialServiceCarriesStatus() { + assertEquals(503, new CredentialServiceException(503, "down").getStatusCode()); + } + + @Test + void credentialAuthAndRateLimitAreAgentspanExceptions() { + assertInstanceOf(AgentspanException.class, new CredentialAuthException("rejected")); + assertInstanceOf(AgentspanException.class, new CredentialRateLimitException()); + } + + @Test + void baseExceptionKeepsMessageAndCause() { + Throwable cause = new IllegalStateException("c"); + AgentspanException e = new AgentspanException("m", cause); + assertEquals("m", e.getMessage()); + assertSame(cause, e.getCause()); + } +} diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/execution/CliCommandExecutorTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/execution/CliCommandExecutorTest.java new file mode 100644 index 000000000..df6d4ab2d --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/execution/CliCommandExecutorTest.java @@ -0,0 +1,163 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.execution; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +/** + * Unit tests for {@link CliCommandExecutor} — the local executor + tokenizer + * behind the auto-injected {@code run_command} tool. No LLM, no server. + * + * Parity target: Python {@code cli_config.py} (shlex), TypeScript + * {@code cli-config.ts} and C# {@code CliTool.Tokenize}. + * + * The execution tests run harmless real commands ({@code echo}, {@code false}) + * and are disabled on Windows where those binaries differ. + */ +class CliCommandExecutorTest { + + // ── Tokenization ────────────────────────────────────────────────────── + + @Test + void tokenize_bareExecutable() { + assertEquals(List.of("git"), CliCommandExecutor.tokenize("git")); + } + + @Test + void tokenize_fullCommandLine() { + assertEquals( + List.of("gh", "repo", "list", "--limit", "5"), CliCommandExecutor.tokenize("gh repo list --limit 5")); + } + + @Test + void tokenize_collapsesRepeatedWhitespace() { + assertEquals(List.of("git", "status", "-s"), CliCommandExecutor.tokenize("git status\t-s")); + } + + @Test + void tokenize_honorsDoubleQuotes() { + // Naive whitespace split would yield ["git","commit","-m","\"hello","world\""]. + assertEquals( + List.of("git", "commit", "-m", "hello world"), + CliCommandExecutor.tokenize("git commit -m \"hello world\"")); + } + + @Test + void tokenize_honorsSingleQuotes() { + assertEquals(List.of("echo", "hello world"), CliCommandExecutor.tokenize("echo 'hello world'")); + } + + @Test + void tokenize_unbalancedQuotesFallBackToWhitespaceSplit() { + assertEquals(List.of("echo", "\"oops"), CliCommandExecutor.tokenize("echo \"oops")); + } + + @Test + void tokenize_emptyAndNull() { + assertTrue(CliCommandExecutor.tokenize("").isEmpty()); + assertTrue(CliCommandExecutor.tokenize(null).isEmpty()); + } + + // ── Validation (no execution) ───────────────────────────────────────── + + @Test + void run_rejectsDisallowedFullCommandLineKeyedOnExecutable() { + Map result = + CliCommandExecutor.run("rm -rf /", null, null, false, List.of("git"), 30, null, false); + assertEquals("error", result.get("status")); + assertTrue( + ((String) result.get("stderr")).contains("Command 'rm' is not allowed"), + "stderr should name the rejected executable: " + result.get("stderr")); + assertNull(result.get("exit_code"), "no process should have run"); + } + + @Test + void run_shellGateBlocksWhenDisabled() { + Map result = CliCommandExecutor.run("echo hi", null, null, true, List.of(), 30, null, false); + assertEquals("error", result.get("status")); + assertTrue(((String) result.get("stderr")).contains("Shell mode is disabled")); + } + + @Test + void run_emptyCommand() { + Map result = CliCommandExecutor.run(" ", null, null, false, List.of(), 30, null, false); + assertEquals("error", result.get("status")); + assertEquals("No command provided.", result.get("stderr")); + } + + // ── Execution ───────────────────────────────────────────────────────── + + @Test + @DisabledOnOs(OS.WINDOWS) + void run_executesFullCommandLine() { + Map result = + CliCommandExecutor.run("echo hello world", null, null, false, List.of("echo"), 30, null, false); + assertEquals("success", result.get("status")); + assertEquals(0, result.get("exit_code")); + assertEquals("hello world", ((String) result.get("stdout")).trim()); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void run_stripsPathPrefixBeforeWhitelistCheck() { + Map result = + CliCommandExecutor.run("/bin/echo ok", null, null, false, List.of("echo"), 30, null, false); + assertEquals("success", result.get("status")); + assertEquals("ok", ((String) result.get("stdout")).trim()); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void run_mergesEmbeddedAndExplicitArgs() { + Map result = CliCommandExecutor.run( + "echo foo", List.of("bar", "baz"), null, false, List.of("echo"), 30, null, false); + assertEquals("success", result.get("status")); + assertEquals("foo bar baz", ((String) result.get("stdout")).trim()); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void run_honorsQuotedArgInCommandLine() { + // The quoted phrase must arrive as a SINGLE argv element, so echo prints + // it once with a single internal space (not as separate quoted tokens). + Map result = + CliCommandExecutor.run("echo \"hello world\"", null, null, false, List.of("echo"), 30, null, false); + assertEquals("success", result.get("status")); + assertEquals("hello world", ((String) result.get("stdout")).trim()); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void run_nonZeroExitReportsError() { + Map result = CliCommandExecutor.run("false", null, null, false, List.of(), 30, null, false); + assertEquals("error", result.get("status")); + assertNotEquals(0, result.get("exit_code")); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void run_commandNotFound() { + Map result = + CliCommandExecutor.run("agentspan_no_such_binary_xyz", null, null, false, List.of(), 30, null, false); + assertEquals("error", result.get("status")); + assertTrue(((String) result.get("stderr")).contains("Command not found"), "stderr: " + result.get("stderr")); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void run_viaInputMapAndConfigOverload() { + CliConfig cfg = CliConfig.builder().allowedCommands(List.of("echo")).build(); + Map input = Map.of("command", "echo hi there"); + Map result = CliCommandExecutor.run(input, cfg); + assertEquals("success", result.get("status")); + assertEquals("hi there", ((String) result.get("stdout")).trim()); + } +} diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/execution/CodeExecutorAsToolTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/execution/CodeExecutorAsToolTest.java new file mode 100644 index 000000000..d0a569636 --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/execution/CodeExecutorAsToolTest.java @@ -0,0 +1,32 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.execution; + +import static org.junit.jupiter.api.Assertions.*; + +import org.conductoross.conductor.ai.model.ToolDef; +import org.junit.jupiter.api.Test; + +/** + * {@link CodeExecutor#asTool()} must propagate the executor's timeout onto the + * generated {@link ToolDef}, so worker registration sizes the Conductor task + * def's responseTimeout to the handler's blocking duration. Building the tool + * does not execute anything — no Docker required. + */ +class CodeExecutorAsToolTest { + + @Test + void asTool_propagates_executor_timeout_to_toolDef() { + DockerCodeExecutor executor = new DockerCodeExecutor("python:3.12-slim", "python", 420); + ToolDef tool = executor.asTool("py_docker_exec", "run python in docker"); + + assertEquals( + 420, + tool.getTimeoutSeconds(), + "asTool() must carry the executor's 420s timeout onto the ToolDef so a " + + "long-running container exec isn't reclaimed at the 300s default. " + + "COUNTERFACTUAL: if timeout isn't propagated, getTimeoutSeconds()==0."); + assertEquals("worker", tool.getToolType()); + } +} diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/frameworks/AdkBridgeTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/frameworks/AdkBridgeTest.java new file mode 100644 index 000000000..782403527 --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/frameworks/AdkBridgeTest.java @@ -0,0 +1,98 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.frameworks; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; + +import org.conductoross.conductor.ai.Agent; +import org.conductoross.conductor.ai.model.ToolDef; +import org.junit.jupiter.api.Test; + +import com.google.adk.agents.BaseAgent; +import com.google.adk.agents.LlmAgent; +import com.google.adk.tools.Annotations; +import com.google.adk.tools.FunctionTool; + +/** + * Server-free unit tests for the Google ADK bridge ({@link AdkBridge#toAgentspan}). + * + *

    Mirrors how Python's framework e2e validates serialization (framework tagging, + * identity, tool extraction with a valid JSON Schema) without needing the model + * provider — ADK uses Gemini, for which no key is configured in this environment, + * so the runtime path is intentionally not exercised here (compile/serialize only). + */ +class AdkBridgeTest { + + /** A FunctionTool target: ADK reflects the method + its {@code @Schema} params. */ + public static class WeatherTool { + public static Map getWeather( + @Annotations.Schema(name = "city", description = "City to look up") String city) { + return Map.of("city", city, "tempF", 72); + } + } + + private LlmAgent buildAdkAgent() { + return LlmAgent.builder() + .name("adk_weather_agent") + .model("gemini-2.0-flash") + .instruction("You report the weather. Use the getWeather tool.") + .tools(FunctionTool.create(WeatherTool.class, "getWeather")) + .build(); + } + + @Test + void toAgentspanTagsFrameworkAndCopiesIdentity() { + Agent a = AdkBridge.toAgentspan(buildAdkAgent()); + assertEquals( + "google_adk", + a.getFramework(), + "ADK agents must be tagged framework='google_adk' so the server routes them through " + + "GoogleADKNormalizer. COUNTERFACTUAL: if the bridge omits the tag, normalization is wrong."); + assertEquals("adk_weather_agent", a.getName(), "agent name must be copied from the ADK LlmAgent"); + assertNotNull(a.getModel(), "model must be carried over from the ADK agent"); + assertTrue( + a.getModel().toLowerCase().contains("gemini"), + "model should carry the ADK model name; got: " + a.getModel()); + } + + @Test + void toAgentspanExtractsFunctionToolWithSchema() { + Agent a = AdkBridge.toAgentspan(buildAdkAgent()); + List tools = a.getTools(); + assertNotNull(tools, "tools list must not be null"); + assertFalse( + tools.isEmpty(), + "AdkBridge must extract the FunctionTool as a worker tool. " + + "COUNTERFACTUAL: if tool extraction is broken, the list is empty."); + + ToolDef wx = tools.stream() + .filter(t -> "getWeather".equals(t.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError("getWeather tool not extracted; got: " + + tools.stream().map(ToolDef::getName).toList())); + + Map schema = wx.getInputSchema(); + assertNotNull(schema, "extracted tool must have an input schema"); + assertEquals( + "object", schema.get("type"), "tool inputSchema.type must be 'object'; got: " + schema.get("type")); + @SuppressWarnings("unchecked") + Map props = (Map) schema.get("properties"); + assertNotNull(props, "schema must have 'properties'"); + assertTrue( + props.containsKey("city"), + "FunctionTool param 'city' (from @Schema) must appear in the input schema; got: " + props.keySet() + + ". COUNTERFACTUAL: if ADK param reflection is dropped, 'city' is missing."); + } + + @Test + void nullAgentRejected() { + assertThrows( + IllegalArgumentException.class, + () -> AdkBridge.agentBuilder((BaseAgent) null), + "agentBuilder(null) must fail fast rather than NPE deeper in serialization"); + } +} diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/handoff/HandoffTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/handoff/HandoffTest.java new file mode 100644 index 000000000..e8a6831f8 --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/handoff/HandoffTest.java @@ -0,0 +1,44 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.handoff; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +/** Pure unit tests for handoff routing rules. */ +class HandoffTest { + + @Test + void textMentionCapturesTriggerAndTarget() { + OnTextMention h = OnTextMention.of("reverse", "text_agent"); + assertEquals("reverse", h.getText()); + assertEquals("text_agent", h.getTarget()); + } + + @Test + void toolResultWithContains() { + OnToolResult h = OnToolResult.of("calc", "math_agent", "42"); + assertEquals("calc", h.getToolName()); + assertEquals("math_agent", h.getTarget()); + assertEquals("42", h.getResultContains()); + } + + @Test + void toolResultTwoArg() { + OnToolResult h = OnToolResult.of("calc", "math_agent"); + assertEquals("calc", h.getToolName()); + assertEquals("math_agent", h.getTarget()); + } + + @Test + void onConditionPredicateEvaluates() { + OnCondition h = new OnCondition("router", m -> "go".equals(m.get("k"))); + assertEquals("router", h.getTarget()); + assertTrue(h.getCondition().apply(Map.of("k", "go"))); + assertFalse(h.getCondition().apply(Map.of("k", "stop"))); + } +} diff --git a/sdk/java/src/test/java/ai/agentspan/internal/ToolRegistryTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/internal/ToolRegistryTest.java similarity index 56% rename from sdk/java/src/test/java/ai/agentspan/internal/ToolRegistryTest.java rename to sdk/java/src/test/java/org/conductoross/conductor/ai/internal/ToolRegistryTest.java index abd59ae2f..ca8cea81e 100644 --- a/sdk/java/src/test/java/ai/agentspan/internal/ToolRegistryTest.java +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/internal/ToolRegistryTest.java @@ -1,16 +1,13 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.internal; +package org.conductoross.conductor.ai.internal; -import ai.agentspan.annotations.Tool; -import ai.agentspan.model.ToolDef; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; -import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; @@ -18,7 +15,9 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; -import static org.junit.jupiter.api.Assertions.*; +import org.conductoross.conductor.ai.annotations.Tool; +import org.conductoross.conductor.ai.model.ToolDef; +import org.junit.jupiter.api.Test; /** * Unit tests for {@link ToolRegistry}. @@ -29,57 +28,88 @@ class ToolRegistryTest { public static class TimeTools { static final AtomicReference seen = new AtomicReference<>(); - @Tool(name = "take_local_date", description = "x") public String takeLocalDate(LocalDate v) { seen.set(v); return "ok"; } - @Tool(name = "take_instant", description = "x") public String takeInstant(Instant v) { seen.set(v); return "ok"; } - @Tool(name = "take_duration", description = "x") public String takeDuration(Duration v) { seen.set(v); return "ok"; } + + @Tool(name = "take_local_date", description = "x") + public String takeLocalDate(LocalDate v) { + seen.set(v); + return "ok"; + } + + @Tool(name = "take_instant", description = "x") + public String takeInstant(Instant v) { + seen.set(v); + return "ok"; + } + + @Tool(name = "take_duration", description = "x") + public String takeDuration(Duration v) { + seen.set(v); + return "ok"; + } } @Test void local_date_received_as_local_date() { TimeTools tools = new TimeTools(); invokeSingleArg(tools, "take_local_date", "v", "2026-05-12"); - assertInstanceOf(LocalDate.class, TimeTools.seen.get(), - "LocalDate parameter c as " + TimeTools.seen.get().getClass()); + assertInstanceOf( + LocalDate.class, + TimeTools.seen.get(), + "LocalDate parameter c as " + TimeTools.seen.get().getClass()); } @Test void instant_received_as_instant() { TimeTools tools = new TimeTools(); invokeSingleArg(tools, "take_instant", "v", "2026-05-12T13:45:00Z"); - assertInstanceOf(Instant.class, TimeTools.seen.get(), - "Instant parameter received as " + TimeTools.seen.get().getClass()); + assertInstanceOf( + Instant.class, + TimeTools.seen.get(), + "Instant parameter received as " + TimeTools.seen.get().getClass()); } @Test void duration_received_as_duration() { TimeTools tools = new TimeTools(); invokeSingleArg(tools, "take_duration", "v", "PT5M"); - assertInstanceOf(Duration.class, TimeTools.seen.get(), - "Duration parameter received as " + TimeTools.seen.get().getClass()); + assertInstanceOf( + Duration.class, + TimeTools.seen.get(), + "Duration parameter received as " + TimeTools.seen.get().getClass()); } // Optional — needs Jdk8Module. public static class OptionalTools { static final AtomicReference seen = new AtomicReference<>(); + @Tool(name = "take_optional_string", description = "x") - public String takeOptionalString(Optional v) { seen.set(v); return "ok"; } + public String takeOptionalString(Optional v) { + seen.set(v); + return "ok"; + } } @Test void optional_string_received_as_optional() { OptionalTools tools = new OptionalTools(); invokeSingleArg(tools, "take_optional_string", "v", "hello"); - assertInstanceOf(Optional.class, OptionalTools.seen.get(), - "Optional parameter received as " + OptionalTools.seen.get().getClass()); + assertInstanceOf( + Optional.class, + OptionalTools.seen.get(), + "Optional parameter received as " + OptionalTools.seen.get().getClass()); } // List public static class ListTools { static final AtomicReference seen = new AtomicReference<>(); + @Tool(name = "take_list_localdate", description = "x") - public String takeListLocalDate(List v) { seen.set(v); return "ok"; } + public String takeListLocalDate(List v) { + seen.set(v); + return "ok"; + } } @Test @@ -90,8 +120,10 @@ void list_of_local_date_elements_are_local_dates() { assertInstanceOf(List.class, got); List list = (List) got; assertFalse(list.isEmpty()); - assertInstanceOf(LocalDate.class, list.get(0), - "List elements arrived as " + list.get(0).getClass()); + assertInstanceOf( + LocalDate.class, + list.get(0), + "List elements arrived as " + list.get(0).getClass()); } // Schemas — what the LLM sees. @@ -99,19 +131,19 @@ void list_of_local_date_elements_are_local_dates() { @Test void schema_for_local_date_should_be_string_format_date() { Map schema = ToolRegistry.typeToJsonSchema(LocalDate.class); - assertEquals("string", schema.get("type"), - "LocalDate schema is " + schema + " — LLM has no idea it must emit an ISO-8601 date"); - assertEquals("date", schema.get("format"), - "LocalDate schema should declare format=date"); + assertEquals( + "string", + schema.get("type"), + "LocalDate schema is " + schema + " — LLM has no idea it must emit an ISO-8601 date"); + assertEquals("date", schema.get("format"), "LocalDate schema should declare format=date"); } - private static T invokeSingleArg(Object toolsInstance, String toolName, - String paramName, Object rawValue) { + private static T invokeSingleArg(Object toolsInstance, String toolName, String paramName, Object rawValue) { List defs = ToolRegistry.fromInstance(toolsInstance); ToolDef def = defs.stream() - .filter(d -> d.getName().equals(toolName)) - .findFirst() - .orElseThrow(() -> new AssertionError("tool not found: " + toolName)); + .filter(d -> d.getName().equals(toolName)) + .findFirst() + .orElseThrow(() -> new AssertionError("tool not found: " + toolName)); Map input = new LinkedHashMap<>(); input.put(paramName, rawValue); diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/internal/WorkerManagerDomainTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/internal/WorkerManagerDomainTest.java new file mode 100644 index 000000000..990385f2b --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/internal/WorkerManagerDomainTest.java @@ -0,0 +1,89 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; +import java.util.function.Function; + +import org.conductoross.conductor.ai.AgentConfig; +import org.junit.jupiter.api.Test; + +import com.netflix.conductor.client.http.ConductorClient; + +/** + * Unit tests (server-free) for WorkerManager's domain-change rebuild signal. + * + *

    Regression for the stateful-domain bug surfaced by e2e {@code + * Suite14StatefulDomain.test_concurrent_stateful_isolation}: a tool worker was first + * registered with no domain (building the runner to poll the default queue), + * then re-registered under the per-execution {@code runId} domain. Because {@code + * register()} early-returned for an already-known task without flagging a + * rebuild, the runner kept polling the default queue while the server enqueued the + * task under the {@code runId} domain — the task sat {@code SCHEDULED} until the run + * timed out after 600s. + */ +class WorkerManagerDomainTest { + + /** + * Dead address: {@code register()}'s task-def upsert fails fast and is swallowed; + * the domain/flag bookkeeping these tests assert on happens regardless of any server. + */ + private WorkerManager newManager() { + return new WorkerManager(new AgentConfig(100, 1), new ConductorClient("http://localhost:1/api")); + } + + private static final Function, Object> NOOP = in -> null; + + @Test + void newTaskFlagsRebuild() { + WorkerManager wm = newManager(); + wm.register("t", NOOP, null); + assertNull(wm.getTaskDomain("t"), "no domain registered"); + assertTrue(wm.isWorkerSetChanged(), "a brand-new task must flag a runner build"); + } + + @Test + void domainChangeOnReregistrationFlagsRebuild() { + WorkerManager wm = newManager(); + + // 1) First registration with NO domain (what runAsync's pre-register used to do). + wm.register("t", NOOP, null); + wm.clearWorkerSetChangedForTest(); // simulate startAll() consuming it + + // 2) Re-register the SAME task under a per-execution runId domain. + wm.register("t", NOOP, "run-abc123"); + assertEquals("run-abc123", wm.getTaskDomain("t"), "taskDomains must reflect the new per-execution domain"); + assertTrue( + wm.isWorkerSetChanged(), + "a CHANGED domain on re-registration MUST flag a rebuild — otherwise the worker " + + "keeps polling the default queue while the server enqueues under the domain, " + + "leaving the task SCHEDULED until the run times out."); + } + + @Test + void sameDomainReregistrationDoesNotFlagRebuild() { + WorkerManager wm = newManager(); + wm.register("t", NOOP, "run-abc123"); + wm.clearWorkerSetChangedForTest(); + + wm.register("t", NOOP, "run-abc123"); // identical domain — handler swap only + assertFalse( + wm.isWorkerSetChanged(), + "re-registering with the SAME domain must NOT force a needless rebuild " + + "(handlers are looked up live by the running worker)"); + } + + @Test + void clearingDomainFlagsRebuild() { + WorkerManager wm = newManager(); + wm.register("t", NOOP, "run-abc123"); + wm.clearWorkerSetChangedForTest(); + + wm.register("t", NOOP, null); // domain removed + assertNull(wm.getTaskDomain("t")); + assertTrue(wm.isWorkerSetChanged(), "removing a domain also changes the queue and must flag a rebuild"); + } +} diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/internal/WorkerManagerThreadCountTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/internal/WorkerManagerThreadCountTest.java new file mode 100644 index 000000000..822f4487a --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/internal/WorkerManagerThreadCountTest.java @@ -0,0 +1,86 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; +import java.util.function.Function; + +import org.conductoross.conductor.ai.AgentConfig; +import org.junit.jupiter.api.Test; + +import com.netflix.conductor.client.http.ConductorClient; + +/** + * Unit tests (server-free) for WorkerManager's thread-count formula. + * + *

    Regression for the MIN_WORKER_THREADS=16 bug: the old formula + * {@code max(configured, max(16, N_workers))} completely ignored the configured + * thread count whenever there were fewer than 16 worker types. This meant that + * {@code AgentConfig(100, 1)} always yielded 16 polling threads, producing 160 + * HTTP requests/second to a SQLite-backed server — 16× the Conductor default + * (1000ms interval, 1 thread). Over 143 sequential agent runs this caused SQLite + * contention that drove up server response times, which combined with infinite HTTP + * timeouts converted transient slow responses into false 600-second timeouts. + * + *

    The new formula is {@code max(configured, MIN_THREADS_PER_WORKER × N_workers)}, + * where MIN_THREADS_PER_WORKER = 1 (enough to make progress without starving). Users + * who want more throughput increase {@code AgentConfig.workerThreadCount}. + */ +class WorkerManagerThreadCountTest { + + private static final Function, Object> NOOP = in -> null; + + private WorkerManager newManager(int configuredThreads) { + return new WorkerManager( + new AgentConfig(1000, configuredThreads), // 1000ms interval, N threads + new ConductorClient("http://localhost:1/api")); + } + + @Test + void configuredOneThreadRespected_withOneWorker() { + WorkerManager wm = newManager(1); + wm.register("t1", NOOP, null); + // 1 worker type: floor = max(1, 1×1) = 1 + // configured = 1 → threadCount = max(1, 1) = 1 + assertEquals( + 1, + wm.computeThreadCount(), + "AgentConfig(_, 1) with 1 worker must yield 1 thread, not 16. " + + "COUNTERFACTUAL: old formula max(1, max(16,1))=16 ignored configured count."); + } + + @Test + void configuredOneThreadRespected_withFourWorkers() { + WorkerManager wm = newManager(1); + wm.register("t1", NOOP, null); + wm.register("t2", NOOP, null); + wm.register("t3", NOOP, null); + wm.register("t4", NOOP, null); + // 4 worker types: floor = max(1, 1×4) = 4 (need 1 thread per type to make progress) + // configured = 1 → threadCount = max(1, 4) = 4 (floor wins; all types can proceed) + assertEquals( + 4, + wm.computeThreadCount(), + "4 worker types need at least 4 threads so each can make progress; " + + "configured=1 is below the floor so floor wins."); + } + + @Test + void higherConfiguredCountWins() { + WorkerManager wm = newManager(20); + wm.register("t1", NOOP, null); + wm.register("t2", NOOP, null); + // 2 worker types: floor = 2; configured = 20 → threadCount = max(20, 2) = 20 + assertEquals(20, wm.computeThreadCount(), "Configured threadCount=20 > floor=2, so configured wins."); + } + + @Test + void threadCountNeverZero() { + WorkerManager wm = newManager(0); + wm.register("t1", NOOP, null); + assertTrue(wm.computeThreadCount() >= 1, "Thread count must be at least 1 even if configured=0."); + } +} diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/internal/WorkerManagerTimeoutTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/internal/WorkerManagerTimeoutTest.java new file mode 100644 index 000000000..757ce0cfc --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/internal/WorkerManagerTimeoutTest.java @@ -0,0 +1,44 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link WorkerManager#effectiveTaskTimeout(int)} — the rule that + * sizes a Conductor task def's timeout/responseTimeout to a handler's configured + * blocking timeout so the server's patience can never drift below the worker's. + */ +class WorkerManagerTimeoutTest { + + @Test + void unconfigured_uses_safe_default() { + assertEquals(300, WorkerManager.effectiveTaskTimeout(0), "0 (unset) must fall back to the 300s default"); + assertEquals(300, WorkerManager.effectiveTaskTimeout(-5), "negative must fall back to the 300s default"); + } + + @Test + void short_timeouts_keep_the_300s_floor() { + assertEquals(300, WorkerManager.effectiveTaskTimeout(30)); + assertEquals(300, WorkerManager.effectiveTaskTimeout(240)); // 240 + 60 == 300 + } + + @Test + void long_timeouts_raise_the_ceiling_above_300_with_slack() { + assertEquals(301, WorkerManager.effectiveTaskTimeout(241)); // 241 + 60 + assertEquals(360, WorkerManager.effectiveTaskTimeout(300)); // 300 + 60 + assertEquals(660, WorkerManager.effectiveTaskTimeout(600)); // 600 + 60 + } + + @Test + void server_patience_always_exceeds_the_handler_timeout() { + for (int t : new int[] {1, 100, 300, 1000, 5000}) { + assertTrue( + WorkerManager.effectiveTaskTimeout(t) >= t + WorkerManager.TASK_TIMEOUT_SLACK_SECONDS, + "effective timeout for " + t + "s must leave at least the slack margin"); + } + } +} diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/model/AgentHandleErrorTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/model/AgentHandleErrorTest.java new file mode 100644 index 000000000..3592cf409 --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/model/AgentHandleErrorTest.java @@ -0,0 +1,83 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.model; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.internal.AgentClient; +import org.conductoross.conductor.ai.internal.AgentStatusResponse; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.conductor.client.http.ConductorClient; + +/** + * Unit tests for {@link AgentHandle#waitForResult} error-handling — specifically the + * consecutive-error fast-fail added to prevent false 600s timeouts. + */ +class AgentHandleErrorTest { + + private static AgentStatusResponse completed(String executionId) { + try { + String json = "{\"executionId\":\"" + executionId + + "\",\"status\":\"COMPLETED\",\"isComplete\":true,\"isRunning\":false}"; + return new ObjectMapper().readValue(json, AgentStatusResponse.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** Stub AgentClient that always throws — simulates a permanently-down server. */ + private static AgentClient alwaysErrorClient() { + return new AgentClient(new ConductorClient("http://localhost:1/api")) { + @Override + public AgentStatusResponse getAgentStatus(String executionId) { + throw new RuntimeException("connection refused"); + } + }; + } + + /** Stub AgentClient that throws once then returns COMPLETED. */ + private static AgentClient oneErrorThenCompleteClient() { + AtomicInteger calls = new AtomicInteger(0); + return new AgentClient(new ConductorClient("http://localhost:1/api")) { + @Override + public AgentStatusResponse getAgentStatus(String executionId) { + if (calls.incrementAndGet() == 1) throw new RuntimeException("transient"); + return completed(executionId); + } + }; + } + + /** + * With the fix: throws after 10 consecutive errors (well under 5s at 1ms poll). + * COUNTERFACTUAL (no fix): the loop never throws early — it runs until the 600s + * wall, which @Timeout(5) catches as a test timeout failure, proving the fix matters. + */ + @Test + @org.junit.jupiter.api.Timeout(5) + void consecutiveErrorsFastFail() { + AgentHandle handle = new AgentHandle("exec-1", alwaysErrorClient(), null); + + RuntimeException ex = assertThrows(RuntimeException.class, () -> handle.waitForResult(600_000, 1)); + + assertTrue( + ex.getMessage().contains("consecutive errors") + || ex.getMessage().contains("connection refused"), + "Exception must mention the root error. Got: " + ex.getMessage() + + ". COUNTERFACTUAL: old code threw 'Agent timed out after 600000ms' hiding the cause."); + } + + @Test + void singleErrorDoesNotFastFail() { + AgentHandle handle = new AgentHandle("exec-2", oneErrorThenCompleteClient(), null); + AgentResult r = assertDoesNotThrow( + () -> handle.waitForResult(10_000, 1), + "A single transient error followed by success must still complete normally."); + assertEquals(AgentStatus.COMPLETED, r.getStatus(), "Status must be COMPLETED after recovery from one error."); + } +} diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/model/ModelTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/model/ModelTest.java new file mode 100644 index 000000000..1844e0d43 --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/model/ModelTest.java @@ -0,0 +1,101 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.model; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; + +import org.conductoross.conductor.ai.enums.AgentStatus; +import org.conductoross.conductor.ai.enums.EventType; +import org.junit.jupiter.api.Test; + +/** Pure unit tests for the model POJOs/value types (no server). */ +class ModelTest { + + @Test + void agentResultGettersAndNullDefaults() { + TokenUsage usage = new TokenUsage(10, 20, 30); + AgentResult r = new AgentResult( + "out", "exec-1", AgentStatus.COMPLETED, List.of(Map.of("name", "t")), List.of(), usage, null); + assertEquals("out", r.getOutput()); + assertEquals("exec-1", r.getExecutionId()); + assertEquals(AgentStatus.COMPLETED, r.getStatus()); + assertEquals(1, r.getToolCalls().size()); + assertSame(usage, r.getTokenUsage()); + + // null status/lists → safe defaults + AgentResult d = new AgentResult(null, "e", null, null, null, null, null); + assertEquals(AgentStatus.COMPLETED, d.getStatus()); + assertNotNull(d.getToolCalls()); + assertTrue(d.getToolCalls().isEmpty()); + assertNotNull(d.getEvents()); + } + + @Test + void agentEventDirectConstructor() { + AgentEvent e = + new AgentEvent(EventType.MESSAGE, "hello", "calc", Map.of("x", 1), "res", "outp", "exec-9", null, null); + assertEquals(EventType.MESSAGE, e.getType()); + assertEquals("hello", e.getContent()); + assertEquals("calc", e.getToolName()); + assertEquals(1, e.getArgs().get("x")); + assertEquals("exec-9", e.getExecutionId()); + } + + @Test + void agentEventFromMapParsesContent() { + AgentEvent e = AgentEvent.fromMap(Map.of("content", "hi there")); + assertEquals("hi there", e.getContent()); + } + + @Test + void tokenUsage() { + TokenUsage u = new TokenUsage(7, 11, 18); + assertEquals(7, u.getPromptTokens()); + assertEquals(11, u.getCompletionTokens()); + assertEquals(18, u.getTotalTokens()); + } + + @Test + void deploymentInfo() { + DeploymentInfo d = new DeploymentInfo("agent-1_wf", "agent-1"); + assertEquals("agent-1_wf", d.getRegisteredName()); + assertEquals("agent-1", d.getAgentName()); + } + + @Test + void promptTemplateOverloads() { + assertEquals("p", new PromptTemplate("p").getName()); + PromptTemplate withVars = new PromptTemplate("p", Map.of("k", "v")); + assertEquals("v", withVars.getVariables().get("k")); + PromptTemplate versioned = new PromptTemplate("p", Map.of(), 3); + assertEquals(3, versioned.getVersion()); + } + + @Test + void guardrailResultFactories() { + assertTrue(GuardrailResult.pass().isPassed()); + GuardrailResult failed = GuardrailResult.fail("bad"); + assertFalse(failed.isPassed()); + assertEquals("bad", failed.getMessage()); + assertEquals("clean", GuardrailResult.fix("clean").getFixedOutput()); + } + + @Test + void prefillToolCall() { + PrefillToolCall p = PrefillToolCall.of("git_status", Map.of("dir", "/tmp")); + assertEquals("git_status", p.getToolName()); + assertEquals("/tmp", p.getArguments().get("dir")); + } + + @Test + void toolContextStateIsMutable() { + ToolContext ctx = new ToolContext("s", "e", "t"); + assertEquals("e", ctx.getExecutionId()); + ctx.getState().put("repo", "x/y"); + assertEquals("x/y", ctx.getState().get("repo")); + } +} diff --git a/sdk/java/src/test/java/ai/agentspan/plans/ContextTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/plans/ContextTest.java similarity index 89% rename from sdk/java/src/test/java/ai/agentspan/plans/ContextTest.java rename to sdk/java/src/test/java/org/conductoross/conductor/ai/plans/ContextTest.java index 8c16235c8..6fa4a655b 100644 --- a/sdk/java/src/test/java/ai/agentspan/plans/ContextTest.java +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/plans/ContextTest.java @@ -1,16 +1,16 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.plans; +package org.conductoross.conductor.ai.plans; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; /** * Java mirror of the Python {@code test_planner_context.py} / TS @@ -22,21 +22,15 @@ class ContextTest { @Test void rejectsNeitherTextNorUrl() { IllegalArgumentException e = assertThrows( - IllegalArgumentException.class, - () -> Context.builder().build() - ); - assertTrue( - e.getMessage().contains("exactly one of text or url"), - "message was: " + e.getMessage() - ); + IllegalArgumentException.class, () -> Context.builder().build()); + assertTrue(e.getMessage().contains("exactly one of text or url"), "message was: " + e.getMessage()); } @Test void rejectsBothTextAndUrl() { IllegalArgumentException e = assertThrows( - IllegalArgumentException.class, - () -> Context.builder().text("x").url("https://y/").build() - ); + IllegalArgumentException.class, + () -> Context.builder().text("x").url("https://y/").build()); assertTrue(e.getMessage().contains("exactly one of text or url")); } diff --git a/sdk/java/src/test/java/ai/agentspan/plans/OpTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/plans/OpTest.java similarity index 56% rename from sdk/java/src/test/java/ai/agentspan/plans/OpTest.java rename to sdk/java/src/test/java/org/conductoross/conductor/ai/plans/OpTest.java index 9d454530b..8130e2409 100644 --- a/sdk/java/src/test/java/ai/agentspan/plans/OpTest.java +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/plans/OpTest.java @@ -1,44 +1,36 @@ // Copyright (c) 2025 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.plans; - -import org.junit.jupiter.api.Test; - -import java.util.Map; +package org.conductoross.conductor.ai.plans; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Map; + +import org.junit.jupiter.api.Test; + class OpTest { @Test void rejectsNeitherArgsNorGenerate() { IllegalArgumentException e = assertThrows( - IllegalArgumentException.class, - () -> Op.builder("write_file").build() - ); - assertTrue( - e.getMessage().contains("exactly one of args or generate"), - "message was: " + e.getMessage() - ); + IllegalArgumentException.class, () -> Op.builder("write_file").build()); + assertTrue(e.getMessage().contains("exactly one of args or generate"), "message was: " + e.getMessage()); } @Test void rejectsBothArgsAndGenerate() { - IllegalArgumentException e = assertThrows( - IllegalArgumentException.class, - () -> Op.builder("write_file") + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> Op.builder("write_file") .args(Map.of("path", "x")) - .generate(Generate.builder().instructions("i").outputSchema("{\"x\":1}").build()) - .build() - ); - assertTrue( - e.getMessage().contains("exactly one of args or generate"), - "message was: " + e.getMessage() - ); + .generate(Generate.builder() + .instructions("i") + .outputSchema("{\"x\":1}") + .build()) + .build()); + assertTrue(e.getMessage().contains("exactly one of args or generate"), "message was: " + e.getMessage()); } @Test @@ -52,8 +44,11 @@ void acceptsArgsOnly() { @Test void acceptsGenerateOnly() { Op op = Op.builder("write_file") - .generate(Generate.builder().instructions("i").outputSchema("{\"x\":1}").build()) - .build(); + .generate(Generate.builder() + .instructions("i") + .outputSchema("{\"x\":1}") + .build()) + .build(); Map j = op.toJson(); assertEquals("write_file", j.get("tool")); assertNotNull(j.get("generate")); diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/plans/PlansTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/plans/PlansTest.java new file mode 100644 index 000000000..5193d8334 --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/plans/PlansTest.java @@ -0,0 +1,73 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.plans; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +/** Pure unit tests for the PLAN_EXECUTE DSL builders (no server). */ +class PlansTest { + + @Test + void refCarriesStepIdAndEquals() { + Ref r = new Ref("step1"); + assertEquals("step1", r.getStepId()); + assertNotNull(r.toJson()); + assertEquals(new Ref("step1"), r); + assertNotEquals(new Ref("other"), r); + } + + @Test + void opSerializes() { + Op op = Op.builder("git").args(Map.of("cmd", "status")).build(); + Map json = op.toJson(); + assertNotNull(json); + assertFalse(json.isEmpty()); + } + + @Test + void actionSerializes() { + assertNotNull(Action.builder("notify").args(Map.of("msg", "hi")).build().toJson()); + } + + @Test + void opRequiresExactlyOneOfArgsOrGenerate() { + // Invariant enforced in Op's constructor. + assertThrows(IllegalArgumentException.class, () -> Op.builder("git").build()); + } + + @Test + void stepWithOperationSerializes() { + Step s = Step.builder("s1") + .operation(Op.builder("git").args(Map.of("cmd", "status")).build()) + .build(); + assertNotNull(s.toJson()); + } + + @Test + void stepParallelAndDependsOn() { + Step s = Step.builder("s2") + .parallel(true) + .dependsOn("s1") + .operation(Op.builder("x").args(Map.of("k", "v")).build()) + .build(); + assertNotNull(s.toJson()); + } + + @Test + void planWithStepsSerializesToJson() { + Plan plan = Plan.builder() + .step(Step.builder("s1") + .operation( + Op.builder("git").args(Map.of("cmd", "status")).build()) + .build()) + .build(); + Map json = plan.toJson(); + assertNotNull(json); + assertTrue(json.containsKey("steps"), "plan json should expose its steps; got keys: " + json.keySet()); + } +} diff --git a/sdk/java/src/test/java/ai/agentspan/schedule/ScheduleIntegrationTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/schedule/ScheduleIntegrationTest.java similarity index 53% rename from sdk/java/src/test/java/ai/agentspan/schedule/ScheduleIntegrationTest.java rename to sdk/java/src/test/java/org/conductoross/conductor/ai/schedule/ScheduleIntegrationTest.java index a837d49e9..5035131ce 100644 --- a/sdk/java/src/test/java/ai/agentspan/schedule/ScheduleIntegrationTest.java +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/schedule/ScheduleIntegrationTest.java @@ -1,14 +1,12 @@ // Copyright (c) 2026 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.schedule; +package org.conductoross.conductor.ai.schedule; -import ai.agentspan.AgentConfig; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.URI; import java.net.http.HttpClient; @@ -20,10 +18,15 @@ import java.util.Map; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +import com.netflix.conductor.client.http.ConductorClient; + +import io.orkes.conductor.client.ApiClient; /** * Integration tests for the Java schedule SDK against the live agentspan-runtime. @@ -32,14 +35,24 @@ @EnabledIf("schedulerAvailable") class ScheduleIntegrationTest { - private static final String SERVER = System.getenv() - .getOrDefault("AGENTSPAN_SERVER_URL", "http://localhost:6767"); - private static final HttpClient HTTP = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(5)) - .build(); + // Base server URL WITHOUT a trailing "/api"; this suite appends "/api/..." itself. + // AGENTSPAN_SERVER_URL conventionally INCLUDES "/api" (see BaseTest), so normalize it + // away here — otherwise every URL gets a double "/api" and the scheduler probe 404s, + // silently skipping the whole suite. + private static final String SERVER = + stripApiSuffix(System.getenv().getOrDefault("AGENTSPAN_SERVER_URL", "http://localhost:6767")); - private static final String AGENT_NAME = "e2e_java_sched_noop_" + - UUID.randomUUID().toString().substring(0, 8); + private static String stripApiSuffix(String url) { + if (url.endsWith("/")) url = url.substring(0, url.length() - 1); + if (url.endsWith("/api")) url = url.substring(0, url.length() - 4); + return url; + } + + private static final HttpClient HTTP = + HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build(); + + private static final String AGENT_NAME = + "e2e_java_sched_noop_" + UUID.randomUUID().toString().substring(0, 8); private static Schedules schedules; @@ -49,7 +62,8 @@ static boolean schedulerAvailable() { HttpRequest.newBuilder() .uri(URI.create(SERVER + "/api/scheduler/schedules")) .timeout(Duration.ofSeconds(3)) - .GET().build(), + .GET() + .build(), HttpResponse.BodyHandlers.ofString()); return r.statusCode() == 200; } catch (Exception e) { @@ -59,8 +73,9 @@ static boolean schedulerAvailable() { @BeforeAll static void registerWorkflow() throws Exception { - AgentConfig cfg = new AgentConfig(SERVER, null, null, 0, 0); - schedules = new Schedules(cfg, HTTP); + ConductorClient cc = + new ApiClient((SERVER.endsWith("/") ? SERVER.substring(0, SERVER.length() - 1) : SERVER) + "/api"); + schedules = new Schedules(cc); String body = "{\"name\":\"" + AGENT_NAME + "\",\"version\":1,\"schemaVersion\":2," + "\"ownerEmail\":\"e2e@agentspan.test\",\"timeoutSeconds\":60,\"timeoutPolicy\":\"TIME_OUT_WF\"," @@ -83,32 +98,47 @@ static void registerWorkflow() throws Exception { @AfterAll static void unregisterWorkflow() throws Exception { if (schedules != null) { - try { schedules.reconcile(AGENT_NAME, List.of()); } catch (Exception ignored) {} + try { + schedules.reconcile(AGENT_NAME, List.of()); + } catch (Exception ignored) { + } } HTTP.send( HttpRequest.newBuilder() .uri(URI.create(SERVER + "/api/metadata/workflow/" + AGENT_NAME + "/1")) - .DELETE().build(), + .DELETE() + .build(), HttpResponse.BodyHandlers.ofString()); } @AfterEach void clean() { - try { schedules.reconcile(AGENT_NAME, List.of()); } catch (Exception ignored) {} + try { + schedules.reconcile(AGENT_NAME, List.of()); + } catch (Exception ignored) { + } } @Test void reconcileCreatesSchedules() { Map input = new HashMap<>(); input.put("k", 1); - schedules.reconcile(AGENT_NAME, List.of( - Schedule.builder().name("daily").cron("0 0 9 * * ?").input(input).build(), - Schedule.builder().name("weekly").cron("0 0 9 * * MON").build() - )); + schedules.reconcile( + AGENT_NAME, + List.of( + Schedule.builder() + .name("daily") + .cron("0 0 9 * * ?") + .input(input) + .build(), + Schedule.builder().name("weekly").cron("0 0 9 * * MON").build())); List infos = schedules.list(AGENT_NAME); assertEquals(2, infos.size()); - ScheduleInfo daily = infos.stream().filter(i -> "daily".equals(i.getShortName())).findFirst().orElseThrow(); + ScheduleInfo daily = infos.stream() + .filter(i -> "daily".equals(i.getShortName())) + .findFirst() + .orElseThrow(); assertEquals(AGENT_NAME + "-daily", daily.getName()); assertEquals("0 0 9 * * ?", daily.getCron()); assertEquals(input, daily.getInput()); @@ -117,23 +147,30 @@ void reconcileCreatesSchedules() { @Test void upsertAndPrune() { - schedules.reconcile(AGENT_NAME, List.of( - Schedule.builder().name("a").cron("0 0 1 * * ?").build(), - Schedule.builder().name("b").cron("0 0 2 * * ?").build() - )); - schedules.reconcile(AGENT_NAME, List.of( - Schedule.builder().name("a").cron("0 0 9 * * ?").build(), - Schedule.builder().name("c").cron("0 0 17 * * ?").build() - )); + schedules.reconcile( + AGENT_NAME, + List.of( + Schedule.builder().name("a").cron("0 0 1 * * ?").build(), + Schedule.builder().name("b").cron("0 0 2 * * ?").build())); + schedules.reconcile( + AGENT_NAME, + List.of( + Schedule.builder().name("a").cron("0 0 9 * * ?").build(), + Schedule.builder().name("c").cron("0 0 17 * * ?").build())); List infos = schedules.list(AGENT_NAME); assertEquals(2, infos.size()); - ScheduleInfo a = infos.stream().filter(i -> "a".equals(i.getShortName())).findFirst().orElseThrow(); + ScheduleInfo a = infos.stream() + .filter(i -> "a".equals(i.getShortName())) + .findFirst() + .orElseThrow(); assertEquals("0 0 9 * * ?", a.getCron()); } @Test void emptyListPurges() { - schedules.reconcile(AGENT_NAME, List.of(Schedule.builder().name("x").cron("0 * * * * ?").build())); + schedules.reconcile( + AGENT_NAME, + List.of(Schedule.builder().name("x").cron("0 * * * * ?").build())); assertEquals(1, schedules.list(AGENT_NAME).size()); schedules.reconcile(AGENT_NAME, List.of()); assertTrue(schedules.list(AGENT_NAME).isEmpty()); @@ -141,23 +178,36 @@ void emptyListPurges() { @Test void nullPreserves() { - schedules.reconcile(AGENT_NAME, List.of(Schedule.builder().name("x").cron("0 * * * * ?").build())); + schedules.reconcile( + AGENT_NAME, + List.of(Schedule.builder().name("x").cron("0 * * * * ?").build())); schedules.reconcile(AGENT_NAME, null); assertEquals(1, schedules.list(AGENT_NAME).size()); } @Test void duplicateNameRaises() { - assertThrows(ScheduleException.NameConflict.class, () -> schedules.reconcile(AGENT_NAME, List.of( - Schedule.builder().name("dup").cron("0 * * * * ?").build(), - Schedule.builder().name("dup").cron("0 0 9 * * ?").build() - ))); + assertThrows( + ScheduleException.NameConflict.class, + () -> schedules.reconcile( + AGENT_NAME, + List.of( + Schedule.builder() + .name("dup") + .cron("0 * * * * ?") + .build(), + Schedule.builder() + .name("dup") + .cron("0 0 9 * * ?") + .build()))); assertTrue(schedules.list(AGENT_NAME).isEmpty()); } @Test void pauseResume() { - schedules.reconcile(AGENT_NAME, List.of(Schedule.builder().name("p").cron("0 0 9 * * ?").build())); + schedules.reconcile( + AGENT_NAME, + List.of(Schedule.builder().name("p").cron("0 0 9 * * ?").build())); String wire = AGENT_NAME + "-p"; assertFalse(schedules.get(wire).isPaused()); schedules.pause(wire, "rate limit"); @@ -168,21 +218,30 @@ void pauseResume() { @Test void pausedOnCreatePreservesState() { - schedules.reconcile(AGENT_NAME, List.of( - Schedule.builder().name("silent").cron("0 0 9 * * ?").paused(true).build())); + schedules.reconcile( + AGENT_NAME, + List.of(Schedule.builder() + .name("silent") + .cron("0 0 9 * * ?") + .paused(true) + .build())); assertTrue(schedules.get(AGENT_NAME + "-silent").isPaused()); } @Test void deleteRemoves() { - schedules.reconcile(AGENT_NAME, List.of(Schedule.builder().name("d").cron("0 * * * * ?").build())); + schedules.reconcile( + AGENT_NAME, + List.of(Schedule.builder().name("d").cron("0 * * * * ?").build())); schedules.delete(AGENT_NAME + "-d"); assertTrue(schedules.list(AGENT_NAME).isEmpty()); } @Test void getAfterDeleteRaises() { - schedules.reconcile(AGENT_NAME, List.of(Schedule.builder().name("g").cron("0 * * * * ?").build())); + schedules.reconcile( + AGENT_NAME, + List.of(Schedule.builder().name("g").cron("0 * * * * ?").build())); String wire = AGENT_NAME + "-g"; schedules.delete(wire); assertThrows(ScheduleException.NotFound.class, () -> schedules.get(wire)); diff --git a/sdk/java/src/test/java/ai/agentspan/schedule/ScheduleTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/schedule/ScheduleTest.java similarity index 79% rename from sdk/java/src/test/java/ai/agentspan/schedule/ScheduleTest.java rename to sdk/java/src/test/java/org/conductoross/conductor/ai/schedule/ScheduleTest.java index a341adaf0..836f32dab 100644 --- a/sdk/java/src/test/java/ai/agentspan/schedule/ScheduleTest.java +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/schedule/ScheduleTest.java @@ -1,20 +1,20 @@ // Copyright (c) 2026 Agentspan // Licensed under the MIT License. See LICENSE file in the project root for details. -package ai.agentspan.schedule; +package org.conductoross.conductor.ai.schedule; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; /** Unit tests for Schedule + Schedules helpers (no network). */ class ScheduleTest { @@ -36,11 +36,14 @@ void full() { Map input = new HashMap<>(); input.put("c", "#eng"); Schedule s = Schedule.builder() - .name("w").cron("0 0 9 * * MON") + .name("w") + .cron("0 0 9 * * MON") .timezone("America/Los_Angeles") .input(input) - .catchup(true).paused(true) - .startAt(1000L).endAt(2000L) + .catchup(true) + .paused(true) + .startAt(1000L) + .endAt(2000L) .description("desc") .build(); assertEquals("America/Los_Angeles", s.getTimezone()); @@ -53,24 +56,35 @@ void full() { @Test void rejectsEmptyName() { - assertThrows(ScheduleException.class, + assertThrows( + ScheduleException.class, () -> Schedule.builder().name("").cron("* * * * * ?").build()); - assertThrows(ScheduleException.class, + assertThrows( + ScheduleException.class, () -> Schedule.builder().name(" ").cron("* * * * * ?").build()); } @Test void rejectsEmptyCron() { - assertThrows(ScheduleException.class, + assertThrows( + ScheduleException.class, () -> Schedule.builder().name("x").cron("").build()); } @Test void rejectsInvertedWindow() { - assertThrows(ScheduleException.class, - () -> Schedule.builder().name("x").cron("* * * * * ?").startAt(2000L).endAt(1000L).build()); - assertThrows(ScheduleException.class, - () -> Schedule.builder().name("x").cron("* * * * * ?").startAt(1000L).endAt(1000L).build()); + assertThrows(ScheduleException.class, () -> Schedule.builder() + .name("x") + .cron("* * * * * ?") + .startAt(2000L) + .endAt(1000L) + .build()); + assertThrows(ScheduleException.class, () -> Schedule.builder() + .name("x") + .cron("* * * * * ?") + .startAt(1000L) + .endAt(1000L) + .build()); } // ── Wire-name prefix/unprefix ───────────────────────────────────── @@ -113,12 +127,18 @@ void toSaveRequestMinimal() { @Test void toSaveRequestFull() { Map input = new LinkedHashMap<>(); - input.put("c", "#eng"); input.put("n", 42); + input.put("c", "#eng"); + input.put("n", 42); Schedule s = Schedule.builder() - .name("w").cron("0 0 9 * * MON") + .name("w") + .cron("0 0 9 * * MON") .timezone("America/Los_Angeles") - .input(input).catchup(true).paused(true) - .startAt(1000L).endAt(2000L).description("desc") + .input(input) + .catchup(true) + .paused(true) + .startAt(1000L) + .endAt(2000L) + .description("desc") .build(); Map req = Schedules.toSaveRequest(s, "digest"); assertEquals("America/Los_Angeles", req.get("zoneId")); @@ -133,10 +153,12 @@ void toSaveRequestFull() { void inputCopiedNotShared() { Map original = new LinkedHashMap<>(); original.put("a", 1); - Schedule s = Schedule.builder().name("x").cron("* * * * * ?").input(original).build(); + Schedule s = + Schedule.builder().name("x").cron("* * * * * ?").input(original).build(); Map req = Schedules.toSaveRequest(s, "agent"); @SuppressWarnings("unchecked") - Map swrInput = (Map) ((Map) req.get("startWorkflowRequest")).get("input"); + Map swrInput = + (Map) ((Map) req.get("startWorkflowRequest")).get("input"); swrInput.put("mutated", true); assertNull(original.get("mutated")); } @@ -186,15 +208,15 @@ void fromWorkflowScheduleDerivesAgentWhenOmitted() { void distinctNamesOk() { Schedules.checkUniqueNames(List.of( Schedule.builder().name("a").cron("* * * * * ?").build(), - Schedule.builder().name("b").cron("* * * * * ?").build() - )); + Schedule.builder().name("b").cron("* * * * * ?").build())); } @Test void duplicateNameRaises() { - assertThrows(ScheduleException.NameConflict.class, () -> Schedules.checkUniqueNames(List.of( - Schedule.builder().name("a").cron("* * * * * ?").build(), - Schedule.builder().name("a").cron("0 0 9 * * ?").build() - ))); + assertThrows( + ScheduleException.NameConflict.class, + () -> Schedules.checkUniqueNames(List.of( + Schedule.builder().name("a").cron("* * * * * ?").build(), + Schedule.builder().name("a").cron("0 0 9 * * ?").build()))); } } diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/termination/TerminationConditionsTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/termination/TerminationConditionsTest.java new file mode 100644 index 000000000..072894333 --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/termination/TerminationConditionsTest.java @@ -0,0 +1,57 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.termination; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** Pure unit tests for termination conditions: builders, toMap wire shape, composition. */ +class TerminationConditionsTest { + + @Test + void maxMessage() { + MaxMessageTermination t = MaxMessageTermination.of(5); + assertEquals(5, t.getMaxMessages()); + assertEquals("max_message", t.toMap().get("type")); + assertEquals(5, t.toMap().get("maxMessages")); + } + + @Test + void stopMessage() { + StopMessageTermination t = StopMessageTermination.of("DONE"); + assertEquals("DONE", t.getStopMessage()); + assertEquals("stop_message", t.toMap().get("type")); + } + + @Test + void textMention() { + TextMentionTermination t = TextMentionTermination.of("bye", true); + assertEquals("bye", t.getText()); + assertTrue(t.isCaseSensitive()); + assertFalse(TextMentionTermination.of("bye").isCaseSensitive()); + } + + @Test + void andComposition() { + TerminationCondition and = MaxMessageTermination.of(3).and(StopMessageTermination.of("x")); + assertInstanceOf(AndTermination.class, and); + assertNotNull(and.toMap()); + } + + @Test + void orComposition() { + TerminationCondition or = MaxMessageTermination.of(3).or(StopMessageTermination.of("x")); + assertInstanceOf(OrTermination.class, or); + assertNotNull(or.toMap()); + } + + @Test + void terminationResult() { + TerminationResult stop = TerminationResult.stop("done"); + assertTrue(stop.isShouldTerminate()); + assertEquals("done", stop.getReason()); + assertFalse(TerminationResult.continueRunning().isShouldTerminate()); + } +} diff --git a/sdk/java/src/test/java/org/conductoross/conductor/ai/tools/ToolsTest.java b/sdk/java/src/test/java/org/conductoross/conductor/ai/tools/ToolsTest.java new file mode 100644 index 000000000..ec5e17c3a --- /dev/null +++ b/sdk/java/src/test/java/org/conductoross/conductor/ai/tools/ToolsTest.java @@ -0,0 +1,68 @@ +// Copyright (c) 2025 Agentspan +// Licensed under the MIT License. See LICENSE file in the project root for details. + +package org.conductoross.conductor.ai.tools; + +import static org.junit.jupiter.api.Assertions.*; + +import org.conductoross.conductor.ai.model.ToolDef; +import org.junit.jupiter.api.Test; + +/** Pure unit tests for the tool builders — name + toolType wiring (no server). */ +class ToolsTest { + + @Test + void httpToolShape() { + ToolDef t = HttpTool.builder() + .name("fetch") + .description("d") + .url("http://x") + .method("GET") + .build(); + assertEquals("fetch", t.getName()); + assertEquals("http", t.getToolType()); + } + + @Test + void httpToolRequiresName() { + assertThrows( + IllegalArgumentException.class, + () -> HttpTool.builder().url("http://x").build()); + } + + @Test + void mcpToolShape() { + ToolDef t = McpTool.builder() + .name("m") + .description("d") + .serverUrl("http://mcp") + .build(); + assertEquals("mcp", t.getToolType()); + assertEquals("m", t.getName()); + } + + @Test + void humanToolShape() { + ToolDef t = HumanTool.create("ask", "d"); + assertEquals("human", t.getToolType()); + assertEquals("ask", t.getName()); + } + + @Test + void pdfToolShape() { + assertEquals("generate_pdf", PdfTool.create("p", "d").getToolType()); + } + + @Test + void waitForMessageToolShape() { + assertEquals( + "pull_workflow_messages", WaitForMessageTool.create("w", "d").getToolType()); + } + + @Test + void imageToolShape() { + ToolDef t = MediaTools.imageTool("img", "d", "openai", "dall-e-3"); + assertEquals("img", t.getName()); + assertNotNull(t.getToolType()); + } +} diff --git a/sdk/python/README.md b/sdk/python/README.md index 5d89528ad..d035edbcc 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -472,7 +472,6 @@ Runnable examples covering every feature: | [`24_code_execution.py`](examples/24_code_execution.py) | Code execution sandboxes | | [`25_semantic_memory.py`](examples/25_semantic_memory.py) | Long-term memory with retrieval | | [`26_opentelemetry_tracing.py`](examples/26_opentelemetry_tracing.py) | OpenTelemetry spans | -| [`27_user_proxy_agent.py`](examples/27_user_proxy_agent.py) | Interactive conversations | | [`28_gpt_assistant_agent.py`](examples/28_gpt_assistant_agent.py) | OpenAI Assistants API wrapper | | [`29_agent_introductions.py`](examples/29_agent_introductions.py) | Agents introduce themselves | | [`30_multimodal_agent.py`](examples/30_multimodal_agent.py) | Vision model analysis | diff --git a/sdk/python/examples/27_user_proxy_agent.py b/sdk/python/examples/27_user_proxy_agent.py deleted file mode 100644 index a34c57e87..000000000 --- a/sdk/python/examples/27_user_proxy_agent.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (c) 2025 Agentspan -# Licensed under the MIT License. See LICENSE file in the project root for details. - -"""UserProxyAgent — human stand-in for interactive conversations. - -Demonstrates ``UserProxyAgent`` which acts as a human proxy in -multi-agent conversations. When it's the proxy's turn, the workflow -pauses for real human input. - -Modes: - - ALWAYS: always pause for human input - - TERMINATE: pause only when conversation would end - - NEVER: auto-respond (useful for testing) - -Requirements: - - Conductor server with LLM support - - AGENTSPAN_SERVER_URL=http://localhost:6767/api as environment variable - - AGENTSPAN_LLM_MODEL=openai/gpt-4o-mini as environment variable -""" - -from agentspan.agents import Agent, AgentRuntime, EventType, Strategy -from settings import settings -from agentspan.agents.ext import UserProxyAgent - -# ── Human proxy ────────────────────────────────────────────────────── - -human = UserProxyAgent( - name="human", - human_input_mode="ALWAYS", -) - -# ── AI assistant ───────────────────────────────────────────────────── - -assistant = Agent( - name="assistant", - model=settings.llm_model, - instructions=( - "You are a helpful coding assistant. Help the user write Python code. " - "Ask clarifying questions when needed." - ), -) - -# ── Round-robin conversation: human and assistant take turns ───────── - -conversation = Agent( - name="pair_programming", - model=settings.llm_model, - agents=[human, assistant], - strategy=Strategy.ROUND_ROBIN, - max_turns=4, # 2 exchanges (human, assistant, human, assistant) -) - - -if __name__ == "__main__": - with AgentRuntime() as runtime: - handle = runtime.start( - conversation, - "Let's write a Python function to sort a list of dictionaries by a key.", - ) - print(f"Started: {handle.execution_id}\n") - - for event in handle.stream(): - if event.type == EventType.THINKING: - print(f" [thinking] {event.content}") - - elif event.type == EventType.TOOL_CALL: - print(f" [tool_call] {event.tool_name}({event.args})") - - elif event.type == EventType.TOOL_RESULT: - print(f" [tool_result] {event.tool_name} -> {str(event.result)[:100]}") - - elif event.type == EventType.WAITING: - status = handle.get_status() - pt = status.pending_tool or {} - schema = pt.get("response_schema", {}) - props = schema.get("properties", {}) - print("\n--- Human input required ---") - response = {} - for field, fs in props.items(): - desc = fs.get("description") or fs.get("title", field) - if fs.get("type") == "boolean": - val = input(f" {desc} (y/n): ").strip().lower() - response[field] = val in ("y", "yes") - else: - response[field] = input(f" {desc}: ").strip() - handle.respond(response) - print() - - elif event.type == EventType.DONE: - print(f"\nDone: {event.output}") - - # Non-interactive alternative (no HITL, will block on human tasks): - # result = runtime.run(assistant, "Write a Python function to sort a list of dictionaries by a key.") - # result.print_result() - - # Production pattern: - # 1. Deploy once during CI/CD: - # runtime.deploy(conversation) - # - # 2. In a separate long-lived worker process: - # runtime.serve(conversation) - diff --git a/sdk/python/examples/README.md b/sdk/python/examples/README.md index 2d009d672..169b2dd5d 100644 --- a/sdk/python/examples/README.md +++ b/sdk/python/examples/README.md @@ -207,7 +207,6 @@ python examples/adk/01_basic_agent.py | 09 | [Human-in-the-Loop](09_human_in_the_loop.py) | Tool approval gate — approve or reject before execution | `approval_required=True` | | 09b | [HITL with Feedback](09b_hitl_with_feedback.py) | Custom feedback via `respond()` — editorial review with revision notes | `handle.respond()` | | 09c | [HITL with Streaming](09c_hitl_streaming.py) | Real-time event stream with approval pauses | `stream()` + `approve()` | -| 27 | [User Proxy Agent](27_user_proxy_agent.py) | Human stand-in agent for interactive conversations | `UserProxyAgent` | ## Guardrails & Safety @@ -340,7 +339,6 @@ Quick lookup — find the right example for any SDK feature: | `SemanticMemory` | 25 | | `TokenUsage` | 23 | | OpenTelemetry tracing | 26 | -| `UserProxyAgent` | 27 | | `GPTAssistantAgent` | 28 | | `@worker_task` as tools | 14 | | `@tool(external=True)` | 33 | diff --git a/sdk/python/examples/kitchen_sink.py b/sdk/python/examples/kitchen_sink.py index c37a00b63..dbc9fcd85 100644 --- a/sdk/python/examples/kitchen_sink.py +++ b/sdk/python/examples/kitchen_sink.py @@ -10,7 +10,7 @@ - All 8 multi-agent strategies - All tool types (worker, http, mcp, api, agent_tool, human, media, RAG) - All guardrail types (regex, llm, custom, external) with all OnFail modes - - HITL (approve, reject, feedback, UserProxyAgent, human_tool) + - HITL (approve, reject, feedback, human_tool) - Memory (conversation + semantic) - Code execution (local, docker, jupyter, serverless) - Credentials (all isolation modes, CredentialFile) @@ -115,7 +115,6 @@ ServerlessCodeExecutor, ExecutionResult, # Extended - UserProxyAgent, GPTAssistantAgent, CallbackHandler, CliConfig, @@ -462,7 +461,7 @@ def safe_search(query: str) -> dict: # ═══════════════════════════════════════════════════════════════════════ # STAGE 5: Editorial Approval # Features: #17 approval_required, #40 approve, #41 reject, -# #42 feedback/respond, #14 human_tool, #65 UserProxyAgent +# #42 feedback/respond, #14 human_tool # ═══════════════════════════════════════════════════════════════════════ @@ -482,19 +481,11 @@ def publish_article(title: str, content: str, platform: str) -> dict: }, ) -editorial_reviewer = UserProxyAgent( - name="editorial_reviewer", - model=settings.llm_model, - instructions="You are the editorial reviewer. Provide feedback on article quality.", - human_input_mode="TERMINATE", -) - editorial_agent = Agent( name="editorial_approval", model=settings.llm_model, instructions="Review the article, ask questions, get approval before publishing.", tools=[publish_article, editorial_question], - agents=[editorial_reviewer], strategy=Strategy.HANDOFF, ) diff --git a/sdk/python/src/agentspan/agents/__init__.py b/sdk/python/src/agentspan/agents/__init__.py index b3e5f7706..1244e9eee 100644 --- a/sdk/python/src/agentspan/agents/__init__.py +++ b/sdk/python/src/agentspan/agents/__init__.py @@ -79,7 +79,7 @@ def get_weather(city: str) -> str: ) # Extended agent types -from agentspan.agents.ext import GPTAssistantAgent, UserProxyAgent +from agentspan.agents.ext import GPTAssistantAgent # Guardrails from agentspan.agents.guardrail import ( @@ -247,7 +247,6 @@ def resolve_credentials(input_data: dict, names: list) -> dict: "VALID_RETRY_POLICIES", "AgentConfig", # Extended agent types - "UserProxyAgent", "GPTAssistantAgent", # Tools "tool", diff --git a/sdk/python/src/agentspan/agents/_internal/token_utils.py b/sdk/python/src/agentspan/agents/_internal/token_utils.py new file mode 100644 index 000000000..7597c2aec --- /dev/null +++ b/sdk/python/src/agentspan/agents/_internal/token_utils.py @@ -0,0 +1,100 @@ +# Copyright (c) 2025 Agentspan +# Licensed under the MIT License. See LICENSE file in the project root for details. + +"""Auth token helpers shared by the sync/async agent API clients and framework adapters. + +Secured Conductor hosts (e.g. orkes) authenticate API calls with a JWT in the +``X-Authorization`` header, minted from an application access key via +``POST {server}/token``. These helpers centralize that mint (with an expiry-aware +process-wide cache) so every HTTP path — agent API, SSE streaming, framework event +pushes — sends the same correct header. Anonymous servers ignore the header. +""" + +from __future__ import annotations + +import base64 +import json +import logging +import threading +from typing import Dict, Optional, Tuple + +logger = logging.getLogger("agentspan.agents.token_utils") + + +def decode_jwt_exp(token: str) -> float: + """Best-effort decode of a JWT's ``exp`` claim (unix seconds). + + Returns 0.0 for opaque tokens, malformed JWTs, or tokens without ``exp`` — + callers treat 0 as "expiry unknown, use until rejected". + """ + try: + parts = token.split(".") + if len(parts) < 2: + return 0.0 + seg = parts[1] + "=" * (-len(parts[1]) % 4) + payload = json.loads(base64.urlsafe_b64decode(seg)) + return float(payload.get("exp", 0) or 0) + except Exception: + return 0.0 + + +# Process-wide mint cache: (server_url, auth_key) -> (token, exp). Framework event +# pushes run on thread pools, so guard with a lock. +_TOKEN_CACHE: Dict[Tuple[str, str], Tuple[str, float]] = {} +_TOKEN_LOCK = threading.Lock() + + +def resolve_agent_api_token( + server_url: str, + api_key: Optional[str] = None, + auth_key: Optional[str] = None, + auth_secret: Optional[str] = None, +) -> Optional[str]: + """Resolve the JWT for agent API calls. + + An explicit ``api_key`` is already a token and returned as-is. Otherwise a JWT is + minted from ``auth_key``/``auth_secret`` via ``POST {server_url}/token`` and cached + until ~expiry. Returns None when no credentials are configured or the mint fails + (anonymous / security-disabled servers). + """ + if api_key: + return api_key + if not auth_key or not auth_secret: + return None + + import time + + cache_key = (server_url.rstrip("/"), auth_key) + with _TOKEN_LOCK: + cached = _TOKEN_CACHE.get(cache_key) + if cached: + token, exp = cached + if exp == 0.0 or time.time() < exp - 30: + return token + + import requests + + url = server_url.rstrip("/") + "/token" + try: + resp = requests.post(url, json={"keyId": auth_key, "keySecret": auth_secret}, timeout=30) + resp.raise_for_status() + token = resp.json().get("token") + except Exception as e: # pragma: no cover - network/credential failures + logger.warning("Failed to mint agent API token: %s", e) + return None + if not token: + return None + with _TOKEN_LOCK: + _TOKEN_CACHE[cache_key] = (token, decode_jwt_exp(token)) + return token + + +def agent_api_auth_headers( + server_url: str, + api_key: Optional[str] = None, + auth_key: Optional[str] = None, + auth_secret: Optional[str] = None, +) -> Dict[str, str]: + """``X-Authorization`` header dict for agent API calls ({} when anonymous).""" + token = resolve_agent_api_token(server_url, api_key, auth_key, auth_secret) + return {"X-Authorization": token} if token else {} diff --git a/sdk/python/src/agentspan/agents/cli_config.py b/sdk/python/src/agentspan/agents/cli_config.py index 9e95c430b..cff17f6d1 100644 --- a/sdk/python/src/agentspan/agents/cli_config.py +++ b/sdk/python/src/agentspan/agents/cli_config.py @@ -71,18 +71,36 @@ class CliConfig: # ── Validation ───────────────────────────────────────────────────────── +def _executable_of(command: str) -> str: + """Return the executable token of *command*. + + Accepts either a bare executable (``"gh"``) or a full command line + (``"gh repo list --limit 5"``) — LLMs frequently pass the latter — and + returns the first token. Falls back to whitespace splitting if the string + is not validly quoted. + """ + if not command: + return command + try: + tokens = shlex.split(command) + except ValueError: + tokens = command.split() + return tokens[0] if tokens else command + + def _validate_cli_command(command: str, allowed_commands: List[str]) -> None: """Validate *command* against the whitelist. - Strips path prefix (``/usr/bin/git`` → ``git``) before checking. - Empty whitelist permits all commands. + Keys off the executable, so both a bare command (``git``) and a full command + line (``git status -s``) validate the same way. Strips path prefix + (``/usr/bin/git`` → ``git``) before checking. Empty whitelist permits all. Raises: - ValueError: If the command is not in the whitelist. + ValueError: If the executable is not in the whitelist. """ if not allowed_commands: return # no restrictions - base = os.path.basename(command) + base = os.path.basename(_executable_of(command)) if base not in allowed_commands: raise ValueError( f"Command '{base}' is not allowed. " @@ -125,8 +143,28 @@ def run_command( "stderr": "No command provided.", } - # Validate against whitelist - _validate_cli_command(command, allowed_commands) + # Models frequently pass the entire command line as `command` + # (e.g. "gh repo list --limit 5") rather than splitting executable/args. + # Tokenize so both styles work: validation keys off the executable and + # execution gets a proper argv. + try: + tokens = shlex.split(command) + except ValueError as e: + return { + "status": "error", + "stdout": "", + "stderr": f"Could not parse command: {e}", + } + if not tokens: + return { + "status": "error", + "stdout": "", + "stderr": "No command provided.", + } + executable = tokens[0] + + # Validate against whitelist (on the executable) + _validate_cli_command(executable, allowed_commands) # Shell gate if shell and not allow_shell: @@ -138,13 +176,16 @@ def run_command( if not isinstance(args, list): args = [str(args)] + # Merge any args embedded in the command line with the explicit args list + argv = tokens[1:] + [str(a) for a in args] + # Resolve working directory effective_cwd = cwd if cwd else working_dir try: if shell: # Build a safe shell command string - cmd_str = command + " " + " ".join(shlex.quote(str(a)) for a in args) + cmd_str = " ".join(shlex.quote(str(a)) for a in [executable] + argv) result = subprocess.run( cmd_str, shell=True, @@ -155,7 +196,7 @@ def run_command( ) else: result = subprocess.run( - [command] + [str(a) for a in args], + [executable] + argv, capture_output=True, text=True, timeout=timeout, diff --git a/sdk/python/src/agentspan/agents/ext.py b/sdk/python/src/agentspan/agents/ext.py index ecafde858..d0148cc87 100644 --- a/sdk/python/src/agentspan/agents/ext.py +++ b/sdk/python/src/agentspan/agents/ext.py @@ -3,103 +3,19 @@ """Extended agent types — specialised agent classes for common patterns. -- :class:`UserProxyAgent` — a human stand-in agent that pauses for real input. - :class:`GPTAssistantAgent` — wraps the OpenAI Assistants API as an Agent. """ from __future__ import annotations import logging -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from agentspan.agents.agent import Agent logger = logging.getLogger("agentspan.agents.ext") -# ── UserProxyAgent ────────────────────────────────────────────────────── - - -class UserProxyAgent(Agent): - """An agent that acts as a stand-in for a human user. - - When it is this agent's turn in a multi-agent conversation, the - workflow pauses with a ``HumanTask`` and waits for real human input. - The human's response becomes the agent's output. - - In single-agent use, it behaves like a human-input gate — useful - for building interactive conversational flows. - - Args: - name: Agent name (default ``"user"``). - human_input_mode: When to request human input: - - ``"ALWAYS"`` — always pause for human input. - - ``"TERMINATE"`` — pause only when the conversation would - otherwise end (e.g. other agents say "TERMINATE"). - - ``"NEVER"`` — never pause (pass-through, useful for testing). - default_response: Response to use when ``human_input_mode="NEVER"`` - (default ``"Continue."``). - model: LLM model (only used if the agent needs to generate - a response when no human is available). - - Example:: - - from agentspan.agents.ext import UserProxyAgent - - user = UserProxyAgent(name="human") - assistant = Agent(name="assistant", model="openai/gpt-4o") - - team = Agent( - name="chat", - model="openai/gpt-4o", - agents=[user, assistant], - strategy="round_robin", - max_turns=6, - ) - """ - - def __init__( - self, - name: str = "user", - human_input_mode: str = "ALWAYS", - default_response: str = "Continue.", - model: str = "openai/gpt-4o", - instructions: Union[str, Callable[..., str]] = "", - **kwargs: Any, - ) -> None: - if human_input_mode not in ("ALWAYS", "TERMINATE", "NEVER"): - raise ValueError( - f"Invalid human_input_mode {human_input_mode!r}. " - "Must be 'ALWAYS', 'TERMINATE', or 'NEVER'" - ) - - self.human_input_mode = human_input_mode - self.default_response = default_response - - # Mark this as a user proxy via metadata - metadata = kwargs.pop("metadata", {}) or {} - metadata["_agent_type"] = "user_proxy" - metadata["_human_input_mode"] = human_input_mode - metadata["_default_response"] = default_response - - if not instructions: - instructions = ( - "You represent the human user in this conversation. " - "Relay the human's input exactly as provided." - ) - - super().__init__( - name=name, - model=model, - instructions=instructions, - metadata=metadata, - **kwargs, - ) - - def __repr__(self) -> str: - return f"UserProxyAgent(name={self.name!r}, mode={self.human_input_mode!r})" - - # ── GPTAssistantAgent ────────────────────────────────────────────────── diff --git a/sdk/python/src/agentspan/agents/frameworks/claude_agent_sdk.py b/sdk/python/src/agentspan/agents/frameworks/claude_agent_sdk.py index 0b9ec3e5a..5fe03ade2 100644 --- a/sdk/python/src/agentspan/agents/frameworks/claude_agent_sdk.py +++ b/sdk/python/src/agentspan/agents/frameworks/claude_agent_sdk.py @@ -21,6 +21,7 @@ from typing import Any, Dict, List, Optional, Tuple from agentspan.agents.frameworks.serializer import WorkerInfo +from agentspan.agents._internal.token_utils import agent_api_auth_headers logger = logging.getLogger("agentspan.agents.frameworks.claude_agent_sdk") @@ -830,11 +831,7 @@ def _do_push(): import requests url = f"{server_url}/agent/events/{execution_id}" - headers: Dict[str, str] = {} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers = agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) requests.post(url, json=event, headers=headers, timeout=5) except Exception as exc: logger.debug("Event push failed (execution_id=%s): %s", execution_id, exc) @@ -871,10 +868,9 @@ def _do_update(): url = f"{server_url}/tasks" headers: Dict[str, str] = {"Content-Type": "application/json"} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers.update( + agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) + ) body = { "taskId": task_id, "workflowInstanceId": execution_id, @@ -917,10 +913,9 @@ def _create_tracking_workflow( url = f"{server_url}/agent/execution" headers: Dict[str, str] = {"Content-Type": "application/json"} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers.update( + agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) + ) body: Dict[str, Any] = {"workflowName": workflow_name, "input": input_data} if parent_workflow_id: body["parentWorkflowId"] = parent_workflow_id @@ -959,10 +954,9 @@ def _inject_tool_task( url = f"{server_url}/agent/{execution_id}/tasks" headers: Dict[str, str] = {"Content-Type": "application/json"} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers.update( + agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) + ) body: Dict[str, Any] = { "taskDefName": tool_name, "referenceTaskName": ref_name, @@ -1003,10 +997,9 @@ def _do_complete(): url = f"{server_url}/agent/tasks/{execution_id}/{ref_name}/{status}" headers: Dict[str, str] = {"Content-Type": "application/json"} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers.update( + agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) + ) requests.post(url, json=output_data, headers=headers, timeout=5) except Exception as exc: logger.debug( @@ -1037,10 +1030,9 @@ def _do_complete(): url = f"{server_url}/agent/execution/{workflow_execution_id}/complete" headers: Dict[str, str] = {"Content-Type": "application/json"} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers.update( + agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) + ) requests.post(url, json=output_data or {}, headers=headers, timeout=5) except Exception as exc: logger.debug( diff --git a/sdk/python/src/agentspan/agents/frameworks/langchain.py b/sdk/python/src/agentspan/agents/frameworks/langchain.py index 4e1cf8085..c1cb49395 100644 --- a/sdk/python/src/agentspan/agents/frameworks/langchain.py +++ b/sdk/python/src/agentspan/agents/frameworks/langchain.py @@ -13,6 +13,7 @@ from langchain_core.callbacks import BaseCallbackHandler from agentspan.agents.frameworks.serializer import WorkerInfo +from agentspan.agents._internal.token_utils import agent_api_auth_headers logger = logging.getLogger("agentspan.agents.frameworks.langchain") @@ -240,11 +241,7 @@ def _do_push(): import requests url = f"{server_url}/agent/events/{execution_id}" - headers = {} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers = agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) requests.post(url, json=event, headers=headers, timeout=5) except Exception as exc: logger.debug("Event push failed (execution_id=%s): %s", execution_id, exc) diff --git a/sdk/python/src/agentspan/agents/frameworks/langgraph.py b/sdk/python/src/agentspan/agents/frameworks/langgraph.py index 7213f0e2e..73ce72b41 100644 --- a/sdk/python/src/agentspan/agents/frameworks/langgraph.py +++ b/sdk/python/src/agentspan/agents/frameworks/langgraph.py @@ -26,6 +26,7 @@ from typing import Any, Dict, List, Optional, Tuple from agentspan.agents.frameworks.serializer import WorkerInfo +from agentspan.agents._internal.token_utils import agent_api_auth_headers logger = logging.getLogger("agentspan.agents.frameworks.langgraph") @@ -1780,11 +1781,7 @@ def _do_push(): import requests url = f"{server_url}/agent/events/{execution_id}" - headers = {} - if auth_key: - headers["X-Auth-Key"] = auth_key - if auth_secret: - headers["X-Auth-Secret"] = auth_secret + headers = agent_api_auth_headers(server_url, auth_key=auth_key, auth_secret=auth_secret) requests.post(url, json=event, headers=headers, timeout=5) except Exception as exc: logger.debug("Event push failed (execution_id=%s): %s", execution_id, exc) diff --git a/sdk/python/src/agentspan/agents/runtime/http_client.py b/sdk/python/src/agentspan/agents/runtime/http_client.py index 571a44fe7..0d2b5843c 100644 --- a/sdk/python/src/agentspan/agents/runtime/http_client.py +++ b/sdk/python/src/agentspan/agents/runtime/http_client.py @@ -20,6 +20,7 @@ import httpx +from agentspan.agents._internal.token_utils import decode_jwt_exp from agentspan.agents.exceptions import _raise_api_error logger = logging.getLogger("agentspan.agents.runtime.http_client") @@ -46,23 +47,43 @@ def __init__( self._auth_key = auth_key self._auth_secret = auth_secret self._client: Optional[httpx.AsyncClient] = None + self._token: str = "" + self._token_exp: float = 0.0 - def _base_headers(self) -> Dict[str, str]: - headers: Dict[str, str] = {} + async def _auth_headers(self) -> Dict[str, str]: + """``X-Authorization`` header for secured hosts (orkes); {} when anonymous. + + An explicit api_key is already a token. Otherwise a JWT is minted from + auth_key/auth_secret via ``POST {server}/token`` and cached until ~expiry. + """ if self._api_key: - headers["Authorization"] = f"Bearer {self._api_key}" - elif self._auth_key: - headers["X-Auth-Key"] = self._auth_key - if self._auth_secret: - headers["X-Auth-Secret"] = self._auth_secret - return headers + return {"X-Authorization": self._api_key} + if not self._auth_key or not self._auth_secret: + return {} + + if self._token and (self._token_exp == 0.0 or time.time() < self._token_exp - 30): + return {"X-Authorization": self._token} + + try: + client = await self._get_client() + resp = await client.post( + f"{self._server_url}/token", + json={"keyId": self._auth_key, "keySecret": self._auth_secret}, + ) + resp.raise_for_status() + token = resp.json().get("token") or "" + except Exception as e: # pragma: no cover - network/credential failures + logger.warning("Failed to mint agent API token: %s", e) + return {} + if not token: + return {} + self._token = token + self._token_exp = decode_jwt_exp(token) + return {"X-Authorization": token} async def _get_client(self) -> httpx.AsyncClient: if self._client is None or self._client.is_closed: - self._client = httpx.AsyncClient( - timeout=httpx.Timeout(30.0, connect=5.0), - headers=self._base_headers(), - ) + self._client = httpx.AsyncClient(timeout=httpx.Timeout(30.0, connect=5.0)) return self._client def _url(self, path: str) -> str: @@ -74,7 +95,7 @@ async def start_agent(self, payload: Dict[str, Any]) -> Dict[str, Any]: """POST /agent/start — start an agent execution.""" client = await self._get_client() url = self._url("/start") - resp = await client.post(url, json=payload) + resp = await client.post(url, json=payload, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -85,7 +106,7 @@ async def deploy_agent(self, payload: Dict[str, Any]) -> Dict[str, Any]: """POST /agent/deploy — deploy agent (compile + register, no execution).""" client = await self._get_client() url = self._url("/deploy") - resp = await client.post(url, json=payload) + resp = await client.post(url, json=payload, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -96,7 +117,7 @@ async def compile_agent(self, config_json: Dict[str, Any]) -> Dict[str, Any]: """POST /agent/compile — compile agent config to agent def.""" client = await self._get_client() url = self._url("/compile") - resp = await client.post(url, json=config_json) + resp = await client.post(url, json=config_json, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -107,7 +128,7 @@ async def get_status(self, execution_id: str) -> Dict[str, Any]: """GET /agent/{id}/status — fetch execution status.""" client = await self._get_client() url = self._url(f"/{execution_id}/status") - resp = await client.get(url) + resp = await client.get(url, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -118,7 +139,7 @@ async def respond(self, execution_id: str, body: Dict[str, Any]) -> None: """POST /agent/{id}/respond — complete a pending human task.""" client = await self._get_client() url = self._url(f"/{execution_id}/respond") - resp = await client.post(url, json=body) + resp = await client.post(url, json=body, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -128,7 +149,7 @@ async def stop(self, execution_id: str) -> None: """POST /agent/{id}/stop — graceful deterministic stop.""" client = await self._get_client() url = self._url(f"/{execution_id}/stop") - resp = await client.post(url) + resp = await client.post(url, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -138,7 +159,7 @@ async def signal(self, execution_id: str, message: str) -> None: """POST /agent/{id}/signal — inject persistent context.""" client = await self._get_client() url = self._url(f"/{execution_id}/signal") - resp = await client.post(url, json={"message": message}) + resp = await client.post(url, json={"message": message}, headers=await self._auth_headers()) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: @@ -152,7 +173,7 @@ async def stream_sse(self, execution_id: str) -> AsyncIterator[Dict[str, Any]]: server doesn't support SSE or sends only heartbeats. """ url = f"{self._server_url}/agent/stream/{execution_id}" - headers = {**self._base_headers(), "Accept": "text/event-stream"} + headers = {**(await self._auth_headers()), "Accept": "text/event-stream"} last_event_id: Optional[str] = None first_connect = True diff --git a/sdk/python/src/agentspan/agents/runtime/runtime.py b/sdk/python/src/agentspan/agents/runtime/runtime.py index ad9255a22..377d236a3 100644 --- a/sdk/python/src/agentspan/agents/runtime/runtime.py +++ b/sdk/python/src/agentspan/agents/runtime/runtime.py @@ -148,6 +148,47 @@ async def _call_user_fn(fn, *args, **kwargs): return await asyncio.to_thread(fn, *args, **kwargs) +def _resolve_loop_iteration(iteration: object) -> int: + """Resolve the current DO_WHILE loop iteration robustly across Conductor cores. + + The compiler wires a worker's ``iteration`` input from the loop counter + reference ``${_loop.iteration}``. Conductor cores disagree on whether + that loop-output reference resolves for tasks executing *inside* the loop + body: the OSS core agentspan compiles against resolves it to the live integer, + but some embedding hosts' cores (e.g. orkes-conductor) leave it unresolved and + deliver ``None`` — which then crashes ``iteration >= max_retries`` comparisons. + + The authoritative per-iteration value is always present on the Task object + (``task.iteration``), set identically by every core (1-based inside a loop, + 0 outside). So: trust a valid integer input when present (preserves the OSS + path exactly, no regression), otherwise fall back to the live task iteration, + and finally to 0. + """ + if isinstance(iteration, bool): + return 0 + if isinstance(iteration, int): + return iteration + if isinstance(iteration, str) and iteration.strip().lstrip("-").isdigit(): + return int(iteration.strip()) + try: + from conductor.client.context.task_context import get_task_context + + live = getattr(get_task_context().task, "iteration", None) + if isinstance(live, int) and not isinstance(live, bool): + return live + except Exception: + # No task context (e.g. unit-test direct call) or core without the field. + pass + return 0 + + +def _decode_jwt_exp(token: str) -> float: + """Best-effort decode of a JWT's `exp` (unix seconds); 0 if opaque/unavailable.""" + from agentspan.agents._internal.token_utils import decode_jwt_exp + + return decode_jwt_exp(token) + + def _normalize_handoff_target(task_ref: str) -> str: """Extract the actual agent name from a Conductor sub-workflow reference. @@ -409,17 +450,33 @@ def _agent_api_url(self, path: str) -> str: base = self._config.server_url.rstrip("/") return f"{base}/agent{path}" + def _agent_api_token(self) -> "Optional[str]": + """Resolve the bearer token for agent runtime API calls (sent as X-Authorization). + + Prefers an explicit ``api_key`` (already a token). Otherwise mints and caches a JWT + from ``auth_key``/``auth_secret`` via ``POST {server}/token`` — the orkes internal-key + (service-account) auth path, the same exchange the worker client and CLI use. Returns + ``None`` when no credentials are configured (anonymous / security-disabled servers). + """ + from agentspan.agents._internal.token_utils import resolve_agent_api_token + + return resolve_agent_api_token( + self._config.server_url, + api_key=self._config.api_key, + auth_key=self._config.auth_key, + auth_secret=self._config.auth_secret, + ) + def _agent_api_headers(self, content_type: str = "application/json") -> Dict[str, str]: """Build headers for agent runtime API requests.""" headers: Dict[str, str] = {} if content_type: headers["Content-Type"] = content_type - if self._config.api_key: - headers["Authorization"] = f"Bearer {self._config.api_key}" - elif self._config.auth_key: - headers["X-Auth-Key"] = self._config.auth_key - if self._config.auth_secret: - headers["X-Auth-Secret"] = self._config.auth_secret + token = self._agent_api_token() + if token: + # orkes accepts X-Authorization (and Authorization: Bearer); the standalone + # anonymous server ignores it. X-Authorization matches the UI/CLI convention. + headers["X-Authorization"] = token return headers def _register_workflow_credentials( @@ -709,11 +766,7 @@ def _compile_via_server(self, agent: Agent) -> Any: server_url = self._config.server_url.rstrip("/") url = f"{server_url}/agent/compile" - headers = {"Content-Type": "application/json"} - if self._config.auth_key: - headers["X-Auth-Key"] = self._config.auth_key - if self._config.auth_secret: - headers["X-Auth-Secret"] = self._config.auth_secret + headers = self._agent_api_headers() payload = {"agentConfig": config_json} response = requests.post(url, json=payload, headers=headers, timeout=30) @@ -1340,6 +1393,7 @@ def make_combined(specs): async def combined_guardrail_worker( content: object = None, iteration: int = 0 ) -> object: + iteration = _resolve_loop_iteration(iteration) if content is None: content_str = "" elif isinstance(content, str): @@ -1420,6 +1474,7 @@ def _register_single_guardrail_worker(self, guardrail, domain: "Optional[str]" = g_name = guardrail.name async def guardrail_worker(content: object = None, iteration: int = 0) -> object: + iteration = _resolve_loop_iteration(iteration) if content is None: content_str = "" elif isinstance(content, str): @@ -1490,6 +1545,7 @@ def _register_stop_when_worker( task_name = f"{agent_name}_stop_when" async def stop_when_worker(result="", iteration: int = 0, messages=None) -> object: + iteration = _resolve_loop_iteration(iteration) context = {"result": result, "messages": messages or [], "iteration": iteration} try: should_stop = await _call_user_fn(stop_when_fn, context) @@ -1583,6 +1639,7 @@ def _register_termination_worker( task_name = f"{agent_name}_termination" async def termination_worker(result: str = "", iteration: int = 0) -> object: + iteration = _resolve_loop_iteration(iteration) context = {"result": result, "messages": [], "iteration": iteration} try: outcome = await _call_user_fn(termination_cond.should_terminate, context) @@ -2235,11 +2292,7 @@ def plan(self, agent: Agent) -> Any: server_url = self._config.server_url.rstrip("/") url = f"{server_url}/agent/compile" - headers = {"Content-Type": "application/json"} - if self._config.auth_key: - headers["X-Auth-Key"] = self._config.auth_key - if self._config.auth_secret: - headers["X-Auth-Secret"] = self._config.auth_secret + headers = self._agent_api_headers() response = requests.post(url, json=payload, headers=headers, timeout=30) try: @@ -3614,10 +3667,9 @@ def _stream_sse(self, execution_id: str) -> Iterator[AgentEvent]: server_url = self._config.server_url.rstrip("/") url = f"{server_url}/agent/stream/{execution_id}" headers: Dict[str, str] = {"Accept": "text/event-stream"} - if self._config.auth_key: - headers["X-Auth-Key"] = self._config.auth_key - if self._config.auth_secret: - headers["X-Auth-Secret"] = self._config.auth_secret + token = self._agent_api_token() + if token: + headers["X-Authorization"] = token last_event_id: Optional[str] = None first_connect = True diff --git a/sdk/python/tests/unit/test_cli_config.py b/sdk/python/tests/unit/test_cli_config.py index 0f4501fd6..a7f32de2f 100644 --- a/sdk/python/tests/unit/test_cli_config.py +++ b/sdk/python/tests/unit/test_cli_config.py @@ -59,6 +59,18 @@ def test_error_message_lists_allowed(self): with pytest.raises(ValueError, match="gh, git"): _validate_cli_command("curl", ["git", "gh"]) + def test_full_command_line_validates_on_executable(self): + # LLMs commonly pass the entire command line as `command`; validation + # must key off the executable (first token), not the whole string. + _validate_cli_command("gh repo list --limit 5", ["gh"]) # no exception + + def test_full_command_line_with_path_executable(self): + _validate_cli_command("/usr/bin/gh repo list", ["gh"]) # no exception + + def test_full_command_line_disallowed_executable_raises(self): + with pytest.raises(ValueError, match="not allowed"): + _validate_cli_command("rm -rf /", ["gh"]) + class TestMakeCliTool: """Test _make_cli_tool factory.""" @@ -119,6 +131,38 @@ def test_basic_execution(self): cwd=None, ) + def test_full_command_line_in_command_is_tokenized(self): + # Reproduces examples/16d_credentials_gh_cli.py: the LLM passes the whole + # command line in `command`. It must validate on `gh` and exec the tokens. + tool_fn = _make_cli_tool(allowed_commands=["gh"]) + with patch("agentspan.agents.cli_config.subprocess.run") as mock_run: + mock_run.return_value = MagicMock(returncode=0, stdout="[]\n", stderr="") + result = tool_fn.__wrapped__( + command="gh repo list agentspan --limit 5 --json name,updatedAt" + ) + assert result["status"] == "success" + mock_run.assert_called_once_with( + ["gh", "repo", "list", "agentspan", "--limit", "5", "--json", "name,updatedAt"], + capture_output=True, + text=True, + timeout=30, + cwd=None, + ) + + def test_command_line_plus_args_list_are_merged(self): + # Executable + some args in `command`, remaining args in the list. + tool_fn = _make_cli_tool(allowed_commands=["gh"]) + with patch("agentspan.agents.cli_config.subprocess.run") as mock_run: + mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") + tool_fn.__wrapped__(command="gh repo list", args=["--limit", "5"]) + mock_run.assert_called_once_with( + ["gh", "repo", "list", "--limit", "5"], + capture_output=True, + text=True, + timeout=30, + cwd=None, + ) + def test_nonzero_exit_code_returns_error_with_output(self): tool_fn = _make_cli_tool(allowed_commands=[]) with patch("agentspan.agents.cli_config.subprocess.run") as mock_run: diff --git a/sdk/python/tests/unit/test_ext.py b/sdk/python/tests/unit/test_ext.py index 34bf62cd7..c27251a22 100644 --- a/sdk/python/tests/unit/test_ext.py +++ b/sdk/python/tests/unit/test_ext.py @@ -1,35 +1,11 @@ # Copyright (c) 2025 Agentspan # Licensed under the MIT License. See LICENSE file in the project root for details. -"""Unit tests for extended agent types — UserProxyAgent, GPTAssistantAgent.""" +"""Unit tests for extended agent types — GPTAssistantAgent.""" from unittest.mock import MagicMock, patch -import pytest - -from agentspan.agents.ext import GPTAssistantAgent, UserProxyAgent - - -class TestUserProxyAgent: - def test_basic_creation(self): - agent = UserProxyAgent() - assert agent.name == "user" - assert agent.human_input_mode == "ALWAYS" - assert agent.metadata["_agent_type"] == "user_proxy" - - def test_custom_mode(self): - agent = UserProxyAgent(name="operator", human_input_mode="TERMINATE") - assert agent.human_input_mode == "TERMINATE" - - def test_invalid_mode(self): - with pytest.raises(ValueError, match="Invalid human_input_mode"): - UserProxyAgent(human_input_mode="INVALID") - - def test_repr(self): - agent = UserProxyAgent(name="user", human_input_mode="ALWAYS") - r = repr(agent) - assert "UserProxyAgent" in r - assert "user" in r +from agentspan.agents.ext import GPTAssistantAgent class TestGPTAssistantAgent: diff --git a/sdk/python/tests/unit/test_http_client.py b/sdk/python/tests/unit/test_http_client.py index b6499fead..c231be43b 100644 --- a/sdk/python/tests/unit/test_http_client.py +++ b/sdk/python/tests/unit/test_http_client.py @@ -17,18 +17,15 @@ # ── Helpers ────────────────────────────────────────────────────────────── -def _make_client(handler) -> AgentHttpClient: - """Create an AgentHttpClient backed by a mock transport.""" - client = AgentHttpClient( - server_url="http://test-server/api", - auth_key="key1", - auth_secret="secret1", - ) - # Override the lazy client with a mock-transport client that includes base headers - client._client = httpx.AsyncClient( - transport=httpx.MockTransport(handler), - headers=client._base_headers(), - ) +def _make_client(handler, **auth) -> AgentHttpClient: + """Create an AgentHttpClient backed by a mock transport. + + Anonymous by default — pass api_key/auth_key/auth_secret to exercise auth. + """ + client = AgentHttpClient(server_url="http://test-server/api", **auth) + # Override the lazy client with a mock-transport client. Auth headers are + # attached per-request by _auth_headers(), not as client defaults. + client._client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) return client @@ -108,20 +105,6 @@ async def handler(request: httpx.Request) -> httpx.Response: await client.close() -@pytest.mark.asyncio -async def test_auth_headers(): - """Auth headers are sent with every request.""" - - async def handler(request: httpx.Request) -> httpx.Response: - assert request.headers.get("x-auth-key") == "key1" - assert request.headers.get("x-auth-secret") == "secret1" - return httpx.Response(200, json={"executionId": "wf-1"}) - - client = _make_client(handler) - await client.start_agent({"prompt": "test"}) - await client.close() - - @pytest.mark.asyncio async def test_http_error_raises(): """Non-2xx responses raise AgentAPIError (wrapping httpx.HTTPStatusError).""" @@ -178,79 +161,88 @@ async def handler(request: httpx.Request) -> httpx.Response: await client.close() # should not raise -# ── api_key Bearer auth (fix #4) ───────────────────────────────────── +# ── Auth: X-Authorization via api_key or minted token (orkes hosts) ────── -def _make_client_with_api_key(handler) -> AgentHttpClient: - """Create an AgentHttpClient with api_key (Bearer auth).""" - client = AgentHttpClient( - server_url="http://test-server/api", - api_key="my-bearer-token", - ) - client._client = httpx.AsyncClient( - transport=httpx.MockTransport(handler), - headers=client._base_headers(), - ) - return client +@pytest.mark.asyncio +async def test_anonymous_sends_no_auth_header(): + """No api_key and no auth_key/secret → no X-Authorization header.""" + + async def handler(request: httpx.Request) -> httpx.Response: + assert "x-authorization" not in request.headers + return httpx.Response(200, json={"executionId": "wf-1"}) + + client = _make_client(handler) + await client.start_agent({"prompt": "test"}) + await client.close() @pytest.mark.asyncio -async def test_api_key_sends_bearer_auth(): - """api_key should produce Authorization: Bearer header.""" +async def test_api_key_sends_x_authorization(): + """An explicit api_key is already a token — sent directly, no /token call.""" async def handler(request: httpx.Request) -> httpx.Response: - assert request.headers.get("authorization") == "Bearer my-bearer-token" - # Should NOT have X-Auth-Key when api_key is used - assert "x-auth-key" not in request.headers + assert request.url.path != "/api/token", "api_key must not mint a token" + assert request.headers.get("x-authorization") == "my-api-token" return httpx.Response(200, json={"executionId": "wf-1"}) - client = _make_client_with_api_key(handler) + client = _make_client(handler, api_key="my-api-token") await client.start_agent({"prompt": "test"}) await client.close() +@pytest.mark.asyncio +async def test_auth_key_mints_token_and_caches_it(): + """auth_key/auth_secret mint a JWT via POST /token, cached across requests.""" + token_calls = {"count": 0} + + async def handler(request: httpx.Request) -> httpx.Response: + if request.url.path == "/api/token": + token_calls["count"] += 1 + body = json.loads(request.content) + assert body == {"keyId": "key1", "keySecret": "secret1"} + return httpx.Response(200, json={"token": "minted-token"}) + assert request.headers.get("x-authorization") == "minted-token" + return httpx.Response(200, json={"executionId": "wf-1"}) + + client = _make_client(handler, auth_key="key1", auth_secret="secret1") + await client.start_agent({"prompt": "one"}) + await client.start_agent({"prompt": "two"}) + # opaque token → exp unknown → cached until rejected; minted exactly once + assert token_calls["count"] == 1 + await client.close() + + @pytest.mark.asyncio async def test_api_key_takes_precedence_over_auth_key(): - """When both api_key and auth_key are set, api_key (Bearer) wins.""" + """When both api_key and auth_key are set, api_key wins and no token is minted.""" async def handler(request: httpx.Request) -> httpx.Response: - assert request.headers.get("authorization") == "Bearer my-api-key" - assert "x-auth-key" not in request.headers + assert request.url.path != "/api/token", "api_key must not mint a token" + assert request.headers.get("x-authorization") == "my-api-key" return httpx.Response(200, json={"executionId": "wf-1"}) - client = AgentHttpClient( - server_url="http://test-server/api", + client = _make_client( + handler, api_key="my-api-key", auth_key="my-auth-key", auth_secret="my-auth-secret", ) - client._client = httpx.AsyncClient( - transport=httpx.MockTransport(handler), - headers=client._base_headers(), - ) await client.start_agent({"prompt": "test"}) await client.close() @pytest.mark.asyncio -async def test_legacy_auth_key_still_works(): - """When api_key is empty, auth_key/auth_secret headers are sent.""" +async def test_token_mint_failure_degrades_to_anonymous(): + """A failing /token endpoint logs a warning and the request proceeds unauthenticated.""" async def handler(request: httpx.Request) -> httpx.Response: - assert request.headers.get("x-auth-key") == "legacy-key" - assert request.headers.get("x-auth-secret") == "legacy-secret" - assert "authorization" not in request.headers + if request.url.path == "/api/token": + return httpx.Response(503, text="token service down") + assert "x-authorization" not in request.headers return httpx.Response(200, json={"executionId": "wf-1"}) - client = AgentHttpClient( - server_url="http://test-server/api", - api_key="", - auth_key="legacy-key", - auth_secret="legacy-secret", - ) - client._client = httpx.AsyncClient( - transport=httpx.MockTransport(handler), - headers=client._base_headers(), - ) - await client.start_agent({"prompt": "test"}) + client = _make_client(handler, auth_key="key1", auth_secret="secret1") + result = await client.start_agent({"prompt": "test"}) + assert result["executionId"] == "wf-1" await client.close() diff --git a/sdk/python/tests/unit/test_new_features.py b/sdk/python/tests/unit/test_new_features.py index d8cf9dbf1..3955eae45 100644 --- a/sdk/python/tests/unit/test_new_features.py +++ b/sdk/python/tests/unit/test_new_features.py @@ -4,7 +4,7 @@ """Unit tests for all new gap-closing features. Tests code executors, swarm strategy, semantic memory, OTel tracing, -manual pattern, agent introductions, UserProxyAgent, GPTAssistantAgent, +manual pattern, agent introductions, GPTAssistantAgent, and handoff conditions. """ @@ -332,61 +332,6 @@ def test_record_token_usage_none_span(self): record_token_usage(None, prompt_tokens=100, completion_tokens=50) -# ── UserProxyAgent ────────────────────────────────────────────────────── - - -class TestUserProxyAgent: - """Test UserProxyAgent.""" - - def test_creation(self): - from agentspan.agents.ext import UserProxyAgent - - agent = UserProxyAgent(name="human") - assert agent.name == "human" - assert agent.human_input_mode == "ALWAYS" - assert agent.metadata["_agent_type"] == "user_proxy" - - def test_default_name(self): - from agentspan.agents.ext import UserProxyAgent - - agent = UserProxyAgent() - assert agent.name == "user" - - def test_terminate_mode(self): - from agentspan.agents.ext import UserProxyAgent - - agent = UserProxyAgent(human_input_mode="TERMINATE") - assert agent.human_input_mode == "TERMINATE" - - def test_never_mode(self): - from agentspan.agents.ext import UserProxyAgent - - agent = UserProxyAgent(human_input_mode="NEVER", default_response="Skip") - assert agent.human_input_mode == "NEVER" - assert agent.default_response == "Skip" - - def test_invalid_mode_raises(self): - from agentspan.agents.ext import UserProxyAgent - - with pytest.raises(ValueError, match="Invalid human_input_mode"): - UserProxyAgent(human_input_mode="SOMETIMES") - - def test_is_agent_subclass(self): - from agentspan.agents.agent import Agent - from agentspan.agents.ext import UserProxyAgent - - agent = UserProxyAgent() - assert isinstance(agent, Agent) - - def test_repr(self): - from agentspan.agents.ext import UserProxyAgent - - agent = UserProxyAgent(name="tester", human_input_mode="ALWAYS") - r = repr(agent) - assert "tester" in r - assert "ALWAYS" in r - - # ── GPTAssistantAgent ────────────────────────────────────────────────── diff --git a/sdk/python/tests/unit/test_sse_client.py b/sdk/python/tests/unit/test_sse_client.py index 18721ebab..fe5d63b98 100644 --- a/sdk/python/tests/unit/test_sse_client.py +++ b/sdk/python/tests/unit/test_sse_client.py @@ -93,10 +93,38 @@ def do_GET(self): except (BrokenPipeError, ConnectionResetError): pass # Client disconnected + def do_POST(self): + # Mint endpoint used by the auth-headers path: POST {server}/token + # with {"keyId", "keySecret"} -> {"token": } (orkes contract). + if self.path.endswith("/token"): + length = int(self.headers.get("Content-Length", 0)) + body = json.loads(self.rfile.read(length)) if length else {} + self.server.mint_requests = getattr(self.server, "mint_requests", []) # type: ignore[attr-defined] + self.server.mint_requests.append(body) # type: ignore[attr-defined] + data = json.dumps({"token": MOCK_MINTED_JWT}).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + return + self.send_error(404) + def log_message(self, format, *args): pass # Suppress request logs during tests +def _mock_jwt() -> str: + import base64 + + header = base64.urlsafe_b64encode(b'{"alg":"HS256"}').rstrip(b"=").decode() + payload = base64.urlsafe_b64encode(b'{"exp":4102444800}').rstrip(b"=").decode() + return f"{header}.{payload}.sig" + + +MOCK_MINTED_JWT = _mock_jwt() + + class MockSSEServer: """Lightweight SSE server for testing.""" @@ -368,7 +396,11 @@ def test_connection_refused_raises_sse_unavailable(self): class TestStreamSSEAuth: - def test_auth_headers_sent(self): + def test_auth_key_secret_mints_x_authorization(self): + """auth_key/auth_secret are exchanged for a JWT via POST /token (the + secured-host contract, e.g. orkes) and sent as X-Authorization.""" + from agentspan.agents._internal.token_utils import _TOKEN_CACHE + scenario = { "events": [ {"event": "done", "id": "1", "data": _java_event("done", output="ok")}, @@ -378,15 +410,22 @@ def test_auth_headers_sent(self): server = MockSSEServer(scenario) url = server.start() try: + _TOKEN_CACHE.clear() rt = _make_runtime(url, auth_key="my-key", auth_secret="my-secret") events = list(rt._stream_sse("test-wf")) assert len(events) == 1 + # The mint endpoint received the key/secret... + mints = getattr(server.server, "mint_requests", []) + assert mints and mints[0] == {"keyId": "my-key", "keySecret": "my-secret"} + # ...and the stream request carried the minted JWT. headers = server.received_headers - assert headers.get("X-Auth-Key") == "my-key" - assert headers.get("X-Auth-Secret") == "my-secret" + assert headers.get("X-Authorization") == MOCK_MINTED_JWT + assert "X-Auth-Key" not in headers + assert "X-Auth-Secret" not in headers finally: server.stop() + _TOKEN_CACHE.clear() def test_no_auth_headers_when_not_configured(self): scenario = { @@ -402,6 +441,7 @@ def test_no_auth_headers_when_not_configured(self): list(rt._stream_sse("test-wf")) headers = server.received_headers + assert "X-Authorization" not in headers assert "X-Auth-Key" not in headers assert "X-Auth-Secret" not in headers finally: diff --git a/sdk/python/tests/unit/test_token_utils.py b/sdk/python/tests/unit/test_token_utils.py new file mode 100644 index 000000000..1d9e95298 --- /dev/null +++ b/sdk/python/tests/unit/test_token_utils.py @@ -0,0 +1,109 @@ +# Copyright (c) 2025 Agentspan +# Licensed under the MIT License. See LICENSE file in the project root for details. + +"""Unit tests for the shared agent-API auth token helpers. + +Uses a real in-process HTTP server (no mocks, per repo test policy) to emulate the +host's POST /token mint endpoint. +""" + +import base64 +import json +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer + +import pytest + +from agentspan.agents._internal.token_utils import ( + _TOKEN_CACHE, + agent_api_auth_headers, + decode_jwt_exp, + resolve_agent_api_token, +) + + +def _jwt(exp: int) -> str: + header = base64.urlsafe_b64encode(b'{"alg":"HS256"}').rstrip(b"=").decode() + payload = base64.urlsafe_b64encode(json.dumps({"exp": exp}).encode()).rstrip(b"=").decode() + return f"{header}.{payload}.sig" + + +class _TokenHandler(BaseHTTPRequestHandler): + mint_count = 0 + token = _jwt(4102444800) # far future + + def do_POST(self): # noqa: N802 + if self.path != "/token": + self.send_response(404) + self.end_headers() + return + length = int(self.headers.get("Content-Length", 0)) + body = json.loads(self.rfile.read(length)) + if body.get("keyId") != "kid" or body.get("keySecret") != "ksec": + self.send_response(401) + self.end_headers() + return + type(self).mint_count += 1 + data = json.dumps({"token": self.token}).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def log_message(self, *args): # silence + pass + + +@pytest.fixture() +def token_server(): + _TokenHandler.mint_count = 0 + srv = HTTPServer(("127.0.0.1", 0), _TokenHandler) + t = threading.Thread(target=srv.serve_forever, daemon=True) + t.start() + url = f"http://127.0.0.1:{srv.server_address[1]}" + yield url + srv.shutdown() + _TOKEN_CACHE.clear() + + +def test_decode_jwt_exp(): + assert decode_jwt_exp(_jwt(1700000000)) == 1700000000.0 + assert decode_jwt_exp("opaque-token") == 0.0 + assert decode_jwt_exp("") == 0.0 + + +def test_api_key_passthrough(): + # An explicit api_key is already a token — no mint, returned as-is. + assert resolve_agent_api_token("http://unused", api_key="tok-123") == "tok-123" + assert agent_api_auth_headers("http://unused", api_key="tok-123") == { + "X-Authorization": "tok-123" + } + + +def test_anonymous_returns_none(): + assert resolve_agent_api_token("http://unused") is None + assert agent_api_auth_headers("http://unused") == {} + + +def test_mint_and_cache(token_server): + tok = resolve_agent_api_token(token_server, auth_key="kid", auth_secret="ksec") + assert tok == _TokenHandler.token + # Second call must hit the cache (no second mint). + tok2 = resolve_agent_api_token(token_server, auth_key="kid", auth_secret="ksec") + assert tok2 == tok + assert _TokenHandler.mint_count == 1 + assert agent_api_auth_headers(token_server, auth_key="kid", auth_secret="ksec") == { + "X-Authorization": tok + } + + +def test_expired_cache_reminted(token_server): + _TOKEN_CACHE[(token_server, "kid")] = (_jwt(100), 100.0) # long expired + tok = resolve_agent_api_token(token_server, auth_key="kid", auth_secret="ksec") + assert tok == _TokenHandler.token + assert _TokenHandler.mint_count == 1 # re-minted exactly once + + +def test_bad_credentials_none(token_server): + assert resolve_agent_api_token(token_server, auth_key="kid", auth_secret="WRONG") is None \ No newline at end of file diff --git a/sdk/python/validation/groups.py b/sdk/python/validation/groups.py index 1de7f58fa..335da885c 100644 --- a/sdk/python/validation/groups.py +++ b/sdk/python/validation/groups.py @@ -38,7 +38,6 @@ "18_manual_selection", "19_composable_termination", "20_constrained_transitions", - "27_user_proxy_agent", "29_agent_introductions", "30_multimodal_agent", "32_human_guardrail", diff --git a/sdk/typescript/examples/27-user-proxy-agent.ts b/sdk/typescript/examples/27-user-proxy-agent.ts deleted file mode 100644 index 6507b80d2..000000000 --- a/sdk/typescript/examples/27-user-proxy-agent.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * UserProxyAgent -- human stand-in for interactive conversations. - * - * Demonstrates `UserProxyAgent` which acts as a human proxy in - * multi-agent conversations. When it's the proxy's turn, the workflow - * pauses for real human input. - * - * Modes: - * - ALWAYS: always pause for human input - * - TERMINATE: pause only when conversation would end - * - NEVER: auto-respond (useful for testing) - * - * Requirements: - * - Conductor server with LLM support - * - AGENTSPAN_SERVER_URL=http://localhost:6767/api as environment variable - * - AGENTSPAN_LLM_MODEL=openai/gpt-4o-mini as environment variable - */ - -import * as readline from 'node:readline/promises'; -import { stdin, stdout } from 'node:process'; -import { Agent, AgentRuntime, UserProxyAgent } from '@agentspan-ai/sdk'; -import type { AgentHandle } from '@agentspan-ai/sdk'; -import { llmModel } from './settings'; - -// -- Human proxy ----------------------------------------------------------- - -const human = new UserProxyAgent({ - name: 'human', - mode: 'ALWAYS', -}); - -// -- AI assistant ---------------------------------------------------------- - -export const assistant = new Agent({ - name: 'assistant', - model: llmModel, - instructions: - 'You are a helpful coding assistant. Help the user write Python code. ' + - 'Ask clarifying questions when needed.', -}); - -// -- Round-robin conversation: human and assistant take turns --------------- - -export const conversation = new Agent({ - name: 'pair_programming', - model: llmModel, - agents: [human, assistant], - strategy: 'round_robin', - maxTurns: 4, // 2 exchanges (human, assistant, human, assistant) -}); - -// -- Helpers ---------------------------------------------------------------- - -async function promptHuman( - rl: readline.Interface, - pendingTool: Record, -): Promise> { - const schema = (pendingTool.response_schema ?? {}) as Record; - const props = (schema.properties ?? {}) as Record>; - const response: Record = {}; - for (const [field, fs] of Object.entries(props)) { - const desc = (fs.description || fs.title || field) as string; - if (fs.type === 'boolean') { - const val = await rl.question(` ${desc} (y/n): `); - response[field] = ['y', 'yes'].includes(val.trim().toLowerCase()); - } else { - response[field] = await rl.question(` ${desc}: `); - } - } - return response; -} - -// -- Run ------------------------------------------------------------------- - -const rl = readline.createInterface({ input: stdin, output: stdout }); -const runtime = new AgentRuntime(); -try { - const handle = await runtime.start( - conversation, - "Let's write a Python function to sort a list of dictionaries by a key.", - ); - console.log(`Started: ${handle.executionId}\n`); - - for await (const event of handle.stream()) { - if (event.type === 'thinking') { - console.log(` [thinking] ${event.content}`); - } else if (event.type === 'tool_call') { - console.log(` [tool_call] ${event.toolName}(${JSON.stringify(event.args)})`); - } else if (event.type === 'tool_result') { - console.log(` [tool_result] ${event.toolName} -> ${JSON.stringify(event.result).slice(0, 100)}`); - } else if (event.type === 'waiting') { - const status = await handle.getStatus(); - const pt = (status.pendingTool ?? {}) as Record; - console.log('\n--- Human input required ---'); - const response = await promptHuman(rl, pt); - await handle.respond(response); - console.log(); - } else if (event.type === 'done') { - console.log(`\nDone: ${JSON.stringify(event.output)}`); - } - } - - // Non-interactive alternative (no HITL, will block on human tasks): - // const result = await runtime.run(assistant, 'Write a Python function to sort a list of dictionaries by a key.'); - // result.printResult(); - - // Production pattern: - // 1. Deploy once during CI/CD: - // await runtime.deploy(conversation); - // - // 2. In a separate long-lived worker process: - // await runtime.serve(conversation); -} finally { - rl.close(); - await runtime.shutdown(); -} diff --git a/sdk/typescript/examples/kitchen-sink.ts b/sdk/typescript/examples/kitchen-sink.ts index e7a1f0433..3c4d52a09 100644 --- a/sdk/typescript/examples/kitchen-sink.ts +++ b/sdk/typescript/examples/kitchen-sink.ts @@ -8,7 +8,7 @@ * - All 8 multi-agent strategies * - All tool types (worker, http, mcp, api, agent_tool, human, media, RAG) * - All guardrail types (regex, llm, custom, external) with all OnFail modes - * - HITL (approve, reject, feedback, UserProxyAgent, human_tool) + * - HITL (approve, reject, feedback, human_tool) * - Memory (conversation + semantic) * - Code execution (local, docker, jupyter, serverless) * - Credentials (all isolation modes, CredentialFile) @@ -116,7 +116,6 @@ import { getCredential, // Extended - UserProxyAgent, GPTAssistantAgent, // Discovery & Tracing @@ -536,7 +535,7 @@ const reviewAgent = new Agent({ // ═══════════════════════════════════════════════════════════════════════ // STAGE 5: Editorial Approval // Features: #17 approval_required, #40 approve, #41 reject, -// #42 feedback/respond, #14 human_tool, #65 UserProxyAgent +// #42 feedback/respond, #14 human_tool // ═══════════════════════════════════════════════════════════════════════ const publishArticle = tool( @@ -569,18 +568,11 @@ const editorialQuestion = humanTool({ }, }); -const editorialReviewer = new UserProxyAgent({ - name: 'editorial_reviewer', - mode: 'TERMINATE', - instructions: 'You are the editorial reviewer. Provide feedback on article quality.', -}); - const editorialAgent = new Agent({ name: 'editorial_approval', model: LLM_MODEL, instructions: 'Review the article, ask questions, get approval before publishing.', tools: [publishArticle, editorialQuestion], - agents: [editorialReviewer], strategy: 'handoff', }); @@ -994,7 +986,6 @@ export { editorialAgent, publishArticle, editorialQuestion, - editorialReviewer, // Stage 6 toneDebate, diff --git a/sdk/typescript/src/cli-config.ts b/sdk/typescript/src/cli-config.ts index 67edb5bb9..f76d5d86a 100644 --- a/sdk/typescript/src/cli-config.ts +++ b/sdk/typescript/src/cli-config.ts @@ -57,20 +57,66 @@ export interface CliConfigOptions { allowShell?: boolean; } +// ── Tokenization ────────────────────────────────────────── + +/** + * Tokenize a command line into argv, honoring single and double quotes. + * + * LLMs frequently pass the whole command line as `command` + * (e.g. `gh repo list --limit 5`) rather than splitting executable/args. + * Falls back to plain whitespace splitting if quotes are unbalanced. + */ +function tokenize(command: string): string[] { + const tokens: string[] = []; + let current = ""; + let hasCurrent = false; + let quote: '"' | "'" | null = null; + + for (const ch of command) { + if (quote) { + if (ch === quote) { + quote = null; + } else { + current += ch; + } + } else if (ch === '"' || ch === "'") { + quote = ch; + hasCurrent = true; + } else if (/\s/.test(ch)) { + if (hasCurrent) { + tokens.push(current); + current = ""; + hasCurrent = false; + } + } else { + current += ch; + hasCurrent = true; + } + } + + if (quote) { + // Unbalanced quotes — fall back to naive whitespace split. + return command.split(/\s+/).filter(Boolean); + } + if (hasCurrent) tokens.push(current); + return tokens; +} + // ── Validation ──────────────────────────────────────────── /** - * Validate a command against the whitelist. - * Strips path prefix (/usr/bin/git -> git) before checking. - * Empty whitelist permits all commands. + * Validate the *executable* of a command against the whitelist. + * + * Keys off the executable token, so both a bare command (`git`) and a full + * command line (`git status -s`) validate the same way. Strips path prefix + * (/usr/bin/git -> git) before checking. Empty whitelist permits all commands. */ -function validateCliCommand(command: string, allowedCommands: string[]): void { +function validateCliCommand(executable: string, allowedCommands: string[]): void { if (!allowedCommands || allowedCommands.length === 0) { return; // no restrictions } - // Strip path prefix - const parts = command.split(/\s+/); - const base = parts[parts.length - 1]; + // Strip path prefix (handles both / and \ separators). + const base = executable.split(/[\\/]/).pop() ?? executable; if (!allowedCommands.includes(base)) { throw new Error( `Command '${base}' is not allowed. ` + @@ -152,8 +198,22 @@ export function makeCliTool(config: CliConfigOptions, agentName: string): ToolDe }; } - // Validate against whitelist - validateCliCommand(command, allowedCommands); + // Models frequently pass the entire command line as `command` + // (e.g. "gh repo list --limit 5") rather than splitting executable/args. + // Tokenize so both styles work: validation keys off the executable and + // execution gets a proper argv. + const tokens = tokenize(command); + if (tokens.length === 0) { + return { + status: "error", + stdout: "", + stderr: "No command provided.", + }; + } + const executable = tokens[0]; + + // Validate against whitelist (on the executable) + validateCliCommand(executable, allowedCommands); // Shell gate const useShell = args.shell === true; @@ -167,12 +227,15 @@ export function makeCliTool(config: CliConfigOptions, agentName: string): ToolDe cmdArgs = [String(cmdArgs)]; } + // Merge any args embedded in the command line with the explicit args list. + const argv = [...tokens.slice(1), ...cmdArgs.map(String)]; + // Resolve working directory const effectiveCwd = (args.cwd as string) || workingDir || undefined; // Use spawnSync to capture both stdout and stderr on success // (execSync only returns stdout, losing stderr from commands like gh clone) - const result = spawnSync(command, cmdArgs.map(String), { + const result = spawnSync(executable, argv, { timeout: timeout * 1000, encoding: "utf-8", cwd: effectiveCwd, @@ -186,7 +249,7 @@ export function makeCliTool(config: CliConfigOptions, agentName: string): ToolDe throw new TerminalToolError(`Command timed out after ${timeout}s`); } if (err.message?.includes("ENOENT")) { - throw new TerminalToolError(`Command not found: ${command}`); + throw new TerminalToolError(`Command not found: ${executable}`); } throw new TerminalToolError(err.message ?? String(err)); } diff --git a/sdk/typescript/src/ext.ts b/sdk/typescript/src/ext.ts index 424e4f776..c8e0d4959 100644 --- a/sdk/typescript/src/ext.ts +++ b/sdk/typescript/src/ext.ts @@ -1,37 +1,6 @@ import { Agent } from "./agent.js"; import type { AgentOptions } from "./agent.js"; -// ── UserProxyAgent ────────────────────────────────────── - -export type UserProxyMode = "ALWAYS" | "TERMINATE" | "NEVER"; - -export interface UserProxyAgentOptions { - name: string; - mode: UserProxyMode; - instructions?: string; -} - -/** - * An agent that proxies user input based on mode. - * - * - ALWAYS: Always prompt the user for input - * - TERMINATE: Prompt user only on termination - * - NEVER: Never prompt the user - */ -export class UserProxyAgent extends Agent { - readonly mode: UserProxyMode; - - constructor(options: UserProxyAgentOptions) { - const agentOptions: AgentOptions = { - name: options.name, - instructions: options.instructions ?? `User proxy agent (mode: ${options.mode})`, - metadata: { userProxy: true, mode: options.mode }, - }; - super(agentOptions); - this.mode = options.mode; - } -} - // ── GPTAssistantAgent ─────────────────────────────────── export interface GPTAssistantAgentOptions { diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 1cba8e8ec..5d10f2564 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -228,8 +228,8 @@ export type { CliConfigOptions } from "./cli-config.js"; export { makeCliTool } from "./cli-config.js"; // ── Extended Agent Types ──────────────────────────────── -export type { UserProxyMode, UserProxyAgentOptions, GPTAssistantAgentOptions } from "./ext.js"; -export { UserProxyAgent, GPTAssistantAgent } from "./ext.js"; +export type { GPTAssistantAgentOptions } from "./ext.js"; +export { GPTAssistantAgent } from "./ext.js"; // ── Discovery ─────────────────────────────────────────── export { discoverAgents } from "./discovery.js"; diff --git a/sdk/typescript/tests/unit/cli-config.test.ts b/sdk/typescript/tests/unit/cli-config.test.ts index 437906c6d..8ff30e5fb 100644 --- a/sdk/typescript/tests/unit/cli-config.test.ts +++ b/sdk/typescript/tests/unit/cli-config.test.ts @@ -241,6 +241,97 @@ describe("makeCliTool", () => { expect(toolContext.state).toEqual({ existing: "value" }); }); + it("accepts a full command line packed into `command` and validates on the executable", async () => { + mockedSpawnSync.mockReturnValue({ + status: 0, + stdout: "repo1\nrepo2\n", + stderr: "", + pid: 1, + output: [], + signal: null, + } as any); + + const tool = makeCliTool({ allowedCommands: ["gh"] }, "test_agent"); + const result = await tool.func!({ command: "gh repo list --limit 5" }); + + expect((result as any).status).toBe("success"); + // Executable tokenized out; remaining tokens become argv. + expect(mockedSpawnSync).toHaveBeenCalledWith( + "gh", + ["repo", "list", "--limit", "5"], + expect.any(Object), + ); + }); + + it("rejects a disallowed full command line keyed on the executable", async () => { + const tool = makeCliTool({ allowedCommands: ["git"] }, "test_agent"); + + await expect(tool.func!({ command: "rm -rf /" })).rejects.toThrow(/Command 'rm' is not allowed/); + }); + + it("strips a path prefix from the executable before whitelist check", async () => { + mockedSpawnSync.mockReturnValue({ + status: 0, + stdout: "ok\n", + stderr: "", + pid: 1, + output: [], + signal: null, + } as any); + + const tool = makeCliTool({ allowedCommands: ["git"] }, "test_agent"); + const result = await tool.func!({ command: "/usr/bin/git status -s" }); + + expect((result as any).status).toBe("success"); + expect(mockedSpawnSync).toHaveBeenCalledWith( + "/usr/bin/git", + ["status", "-s"], + expect.any(Object), + ); + }); + + it("merges args embedded in the command line with the explicit args list", async () => { + mockedSpawnSync.mockReturnValue({ + status: 0, + stdout: "ok\n", + stderr: "", + pid: 1, + output: [], + signal: null, + } as any); + + const tool = makeCliTool({ allowedCommands: ["git"] }, "test_agent"); + const result = await tool.func!({ command: "git commit", args: ["-m", "msg"] }); + + expect((result as any).status).toBe("success"); + expect(mockedSpawnSync).toHaveBeenCalledWith( + "git", + ["commit", "-m", "msg"], + expect.any(Object), + ); + }); + + it("honors quoted arguments in the command line", async () => { + mockedSpawnSync.mockReturnValue({ + status: 0, + stdout: "ok\n", + stderr: "", + pid: 1, + output: [], + signal: null, + } as any); + + const tool = makeCliTool({ allowedCommands: ["git"] }, "test_agent"); + const result = await tool.func!({ command: 'git commit -m "hello world"' }); + + expect((result as any).status).toBe("success"); + expect(mockedSpawnSync).toHaveBeenCalledWith( + "git", + ["commit", "-m", "hello world"], + expect.any(Object), + ); + }); + it("context_key _state_updates does not corrupt internals", async () => { mockedSpawnSync.mockReturnValue({ status: 0, diff --git a/sdk/typescript/tests/unit/kitchen-sink-structural.test.ts b/sdk/typescript/tests/unit/kitchen-sink-structural.test.ts index 55bd3151f..07439639b 100644 --- a/sdk/typescript/tests/unit/kitchen-sink-structural.test.ts +++ b/sdk/typescript/tests/unit/kitchen-sink-structural.test.ts @@ -50,7 +50,6 @@ import { editorialAgent, publishArticle, editorialQuestion, - editorialReviewer, // Stage 6 toneDebate, @@ -79,7 +78,7 @@ import { import { Agent } from "../../src/agent.js"; import { RegexGuardrail, LLMGuardrail } from "../../src/guardrail.js"; -import { UserProxyAgent, GPTAssistantAgent } from "../../src/ext.js"; +import { GPTAssistantAgent } from "../../src/ext.js"; import { TextMention, MaxMessage, @@ -335,14 +334,8 @@ describe("Stage 5: Editorial Approval", () => { expect(editorialQuestion.toolType).toBe("human"); }); - it("editorial reviewer is a UserProxyAgent", () => { - expect(editorialReviewer).toBeInstanceOf(UserProxyAgent); - expect(editorialReviewer.mode).toBe("TERMINATE"); - }); - - it("editorial agent has both tools and sub-agents", () => { + it("editorial agent has tools", () => { expect(editorialAgent.tools).toHaveLength(2); - expect(editorialAgent.agents).toHaveLength(1); }); }); diff --git a/sdk/typescript/vitest.config.ts b/sdk/typescript/vitest.config.ts index 373dea648..acc70446d 100644 --- a/sdk/typescript/vitest.config.ts +++ b/sdk/typescript/vitest.config.ts @@ -18,15 +18,14 @@ export default defineConfig({ test: { globals: true, testTimeout: 60_000, - // Limit to 2 concurrent test files. GitHub Actions ubuntu-latest now uses - // 4-core runners, causing vitest to default to 3 forks. Suites 17, 18, and 20 - // each fire 20–27 concurrent LLM-backed workflows; at 3 forks all three overlap, - // saturating the shared Conductor server. Cap at 2 so at most two heavy suites - // compete at once while keeping meaningful parallelism for the 15 min target. + // Run 3 test files concurrently. Credential names are unique per suite so + // suites 1-5 don't conflict with each other. Suites 17/18 don't use + // credentials. 3 forks cuts wall-clock roughly in half vs 2 forks while + // keeping server load manageable on the shared SQLite-backed Conductor. pool: 'forks', poolOptions: { forks: { - maxForks: 2, + maxForks: 3, minForks: 1, }, }, diff --git a/server/build.gradle b/server/build.gradle index da88d1b75..23e72c900 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,176 +1,98 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.3.5' - id 'io.spring.dependency-management' version '1.1.7' + id 'org.springframework.boot' version '3.3.5' apply false + id 'io.spring.dependency-management' version '1.1.7' apply false id 'com.diffplug.spotless' version '7.0.2' + id 'com.vanniktech.maven.publish' version '0.34.0' apply false } -def uiDir = file("${projectDir}/../ui") -def uiDistDir = file("${uiDir}/dist") -def serverStaticDir = file("${projectDir}/src/main/resources/static") -def buildUiProperty = project.findProperty('buildUI') -def buildUiEnabled = buildUiProperty != null && ( - buildUiProperty.toString().trim().isEmpty() || - buildUiProperty.toString().toBoolean() -) -def nodeOptionsWithMoreHeap = { - def current = System.getenv('NODE_OPTIONS') - current ? "${current} --max-old-space-size=4096" : '--max-old-space-size=4096' -} -def shellCommand = { String command -> - org.gradle.internal.os.OperatingSystem.current().isWindows() - ? ['cmd', '/c', command] - : ['bash', '-lc', command] -} -def pnpmCommand = { String args -> - org.gradle.internal.os.OperatingSystem.current().isWindows() - ? "where pnpm >NUL 2>NUL && pnpm ${args} || (where corepack >NUL 2>NUL && corepack pnpm ${args} || (echo pnpm or corepack is required to build the UI & exit /b 127))" - : "if command -v pnpm >/dev/null 2>&1; then pnpm ${args}; elif command -v corepack >/dev/null 2>&1; then corepack pnpm ${args}; else echo 'pnpm or corepack is required to build the UI' >&2; exit 127; fi" +// Root project needs repositories too — spotless (applied here) resolves +// the palantir-java-format artifact at the root level. +repositories { + mavenCentral() + mavenLocal() } // ── Version catalog ────────────────────────────────────────────── ext { conductorVersion = '3.30.2' lombokVersion = '1.18.42' - log4jVersion = '2.24.3' // managed by Spring BOM, explicit for clarity + log4jVersion = '2.24.3' sqliteJdbcVersion = '3.47.0.0' - springSecVersion = '6.3.5' // bumped for GHSA-mg83-c7gq-rv5c (BCrypt 72-char) + springSecVersion = '6.3.5' jsonSchemaVersion = '1.0.73' junitVersion = '5.10.2' assertjVersion = '3.25.1' mockitoVersion = '5.10.0' } -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } -} - -repositories { - mavenCentral() - mavenLocal() -} - -springBoot { - mainClass = 'dev.agentspan.runtime.AgentRuntime' - buildInfo() -} - -dependencies { - - // Conductor core - implementation "org.conductoross:conductor-core:${conductorVersion}" - implementation "org.conductoross:conductor-rest:${conductorVersion}" - implementation "org.conductoross:conductor-common:${conductorVersion}" - implementation "org.conductoross:conductor-ai:${conductorVersion}" - - // PostgreSQL persistence + queue + indexing + locking - implementation "org.conductoross:conductor-postgres-persistence:${conductorVersion}" - implementation "org.conductoross:conductor-postgres-external-storage:${conductorVersion}" - - // SQLite persistence (activate with conductor.db.type=sqlite) - implementation "org.conductoross:conductor-sqlite-persistence:${conductorVersion}" - - // Scheduler — cron-based workflow scheduling - implementation "org.conductoross:conductor-scheduler-core:${conductorVersion}" - implementation "org.conductoross:conductor-scheduler-sqlite-persistence:${conductorVersion}" - implementation "org.conductoross:conductor-scheduler-postgres-persistence:${conductorVersion}" - - // Common system tasks - implementation "org.conductoross:conductor-http-task:${conductorVersion}" - implementation "org.conductoross:conductor-json-jq-task:${conductorVersion}" - - // Swagger / OpenAPI UI - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.6.0' - - // Spring Boot - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'org.springframework.retry:spring-retry' - annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' - - // Logging - implementation 'org.springframework.boot:spring-boot-starter-log4j2' - implementation 'org.apache.logging.log4j:log4j-web' - - compileOnly "org.projectlombok:lombok:${lombokVersion}" - annotationProcessor "org.projectlombok:lombok:${lombokVersion}" - - // JSON Schema - implementation "com.networknt:json-schema-validator:${jsonSchemaVersion}" - - // SQLite JDBC driver (for credential DataSource) - implementation "org.xerial:sqlite-jdbc:${sqliteJdbcVersion}" - // Spring Security Crypto (BCrypt password hashing, no full Security stack) - implementation "org.springframework.security:spring-security-crypto:${springSecVersion}" - - // Test dependencies - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}" - testImplementation "org.assertj:assertj-core:${assertjVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" - testCompileOnly "org.projectlombok:lombok:${lombokVersion}" - testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" - - // GraalVM polyglot API — needed to compile/run the PLAN_EXECUTE compiler - // script tests (SynthOutputScriptTest, EnrichToolsScriptTest). Runtime jars - // come transitively via conductor-graalvm, but the compile-only API jar - // must be on the test classpath so javac can resolve org.graalvm.polyglot. - testImplementation 'org.graalvm.polyglot:polyglot:25.0.2' - testImplementation 'org.graalvm.js:js:25.0.2' - - // Logging - implementation('org.apache.logging.log4j:log4j-core') - implementation('org.apache.logging.log4j:log4j-api') - implementation('org.apache.logging.log4j:log4j-slf4j-impl') - implementation('org.apache.logging.log4j:log4j-jul') - implementation('org.apache.logging.log4j:log4j-web') - -} - -configurations.all { - exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' - - // GraalVM polyglot engines — Python (~144MB), JS (~62MB), Truffle runtime - // Conductor uses these for inline script evaluation; agents use HTTP instead - exclude group: 'org.graalvm.python' - - // Oracle Coherence vector store (~22MB) — not used - exclude group: 'com.oracle.coherence.ce' +subprojects { + apply plugin: 'java' + apply plugin: 'io.spring.dependency-management' - // Google Cloud Vertex AI / gRPC protos (~69MB) — not used - //exclude group: 'com.google.cloud', module: 'google-cloud-aiplatform' + // Published Maven coordinates: org.conductoross.conductor::. + // Version comes from gradle.properties (default) or -Pversion=X in CI. + group = 'org.conductoross' + version = rootProject.version - // Azure Application Insights (~7MB) — not used - exclude group: 'com.microsoft.azure', module: 'applicationinsights-core' - - // Conscrypt (~4.5MB) — alternate TLS provider, JDK built-in is fine - exclude group: 'org.conscrypt' + java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + } - // Nashorn JS engine (~2MB) — not needed without inline scripts - exclude group: 'org.openjdk.nashorn' + repositories { + mavenCentral() + mavenLocal() + } - // ICU4J (~14MB) — Unicode library pulled by Groovy, JDK handles this - exclude group: 'com.ibm.icu' + configurations.all { + // conductor-ai -> spring-ai -> mcp-json-jackson2 drags in json-schema-validator 2.0.0, + // which removed com.networknt.schema.JsonSchema. conductor-common's JsonSchemaValidator + // bean is compiled against the 1.x class, so pin to the version conductor needs. + resolutionStrategy { + force "com.networknt:json-schema-validator:${jsonSchemaVersion}" + } - // BouncyCastle (~9MB) — crypto provider, JDK built-in is sufficient - exclude group: 'org.bouncycastle' + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' + exclude group: 'org.graalvm.python' + exclude group: 'com.oracle.coherence.ce' + exclude group: 'com.microsoft.azure', module: 'applicationinsights-core' + exclude group: 'org.conscrypt' + exclude group: 'org.openjdk.nashorn' + exclude group: 'com.ibm.icu' + exclude group: 'org.bouncycastle' + exclude group: 'io.micrometer', module: 'micrometer-registry-statsd' + exclude group: 'io.micrometer', module: 'micrometer-registry-atlas' + exclude group: 'com.netflix.spectator' + exclude group: 'org.apache.groovy' + } - // Micrometer StatsD + Atlas + Spectator (~8MB) — we keep basic actuator metrics only - exclude group: 'io.micrometer', module: 'micrometer-registry-statsd' - exclude group: 'io.micrometer', module: 'micrometer-registry-atlas' - exclude group: 'com.netflix.spectator' + test { + useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + events 'failed' + } + } - // Groovy - exclude group: 'org.apache.groovy' + // vanniktech publishing builds a javadoc jar (required by Maven Central). + // Relax doclint, and don't fail on errors: Lombok's @RequiredArgsConstructor + // (onConstructor_) isn't understood by the javadoc tool, which would otherwise + // abort jar generation. The jar is still produced for what resolves. + tasks.withType(Javadoc).configureEach { + options.addStringOption('Xdoclint:none', '-quiet') + failOnError = false + } } // ── Code formatting ────────────────────────────────────────────── spotless { java { - target 'src/main/java/**/*.java', 'src/test/**/*.java' + target fileTree('.') { + include '**/src/main/java/**/*.java', '**/src/test/**/*.java' + exclude '**/build/**' + } palantirJavaFormat('2.50.0') removeUnusedImports() importOrder('java', 'javax', 'jakarta', 'org', 'com', 'dev') @@ -180,23 +102,21 @@ spotless { } // ── Detect fully qualified names used inline ───────────────────── -// Catches patterns like: new com.foo.Bar(), java.util.List<>, etc. -// in code (not import statements). Fails the build with file:line. tasks.register('checkNoInlineFQN') { group = 'verification' description = 'Detect fully qualified class names used inline (should be imports)' doLast { def violations = [] def fqnPattern = ~/(?:new\s+|[\s(,<])(?:java|javax|jakarta|com|org|net)\.[a-z]+(?:\.[a-z]+)*\.[A-Z]\w+/ - fileTree('src/main/java').matching { include '**/*.java' }.each { file -> - file.eachLine { line, lineNum -> - if (line.trim().startsWith('import ') || line.trim().startsWith('package ') - || line.trim().startsWith('//') || line.trim().startsWith('*')) { - return // skip imports, package, comments - } - def matcher = fqnPattern.matcher(line) - if (matcher.find()) { - violations << "${file.path}:${lineNum}: ${line.trim()}" + subprojects.each { sub -> + fileTree("${sub.projectDir}/src/main/java").matching { include '**/*.java' }.each { file -> + file.eachLine { line, lineNum -> + if (line.trim().startsWith('import ') || line.trim().startsWith('package ') + || line.trim().startsWith('//') || line.trim().startsWith('*')) return + def matcher = fqnPattern.matcher(line) + if (matcher.find()) { + violations << "${file.path}:${lineNum}: ${line.trim()}" + } } } } @@ -209,70 +129,6 @@ tasks.register('checkNoInlineFQN') { } } -// Run both spotless and FQN check as part of the build tasks.named('check') { dependsOn 'spotlessCheck', 'checkNoInlineFQN' } - -test { - useJUnitPlatform() -} - -tasks.register('installUiDependencies', Exec) { - group = 'build' - description = 'Install UI dependencies with pnpm when the UI workspace is available' - onlyIf { uiDir.exists() } - workingDir uiDir - commandLine shellCommand(pnpmCommand('install --frozen-lockfile')) - environment 'CI', 'true' - inputs.files( - file("${uiDir}/package.json"), - file("${uiDir}/pnpm-lock.yaml") - ) - outputs.file(file("${uiDir}/node_modules/.modules.yaml")) -} - -tasks.register('buildUi', Exec) { - group = 'build' - description = 'Build the latest UI bundle for the embedded server UI (opt-in via -PbuildUI for bootJar)' - onlyIf { uiDir.exists() } - dependsOn 'installUiDependencies' - workingDir uiDir - commandLine shellCommand(pnpmCommand('build')) - environment 'CI', 'true' - environment 'NODE_OPTIONS', nodeOptionsWithMoreHeap() - inputs.files( - file("${uiDir}/package.json"), - file("${uiDir}/pnpm-lock.yaml"), - file("${uiDir}/index.html"), - file("${uiDir}/vite.config.ts"), - file("${uiDir}/vite-plugin-csp-nonce.ts"), - file("${uiDir}/tsconfig.json") - ) - inputs.dir(file("${uiDir}/src")) - inputs.dir(file("${uiDir}/public")) - outputs.dir(uiDistDir) -} - -tasks.register('syncUiStatic', Sync) { - group = 'build' - description = 'Sync the latest built UI assets into server/src/main/resources/static' - onlyIf { uiDir.exists() } - dependsOn 'buildUi' - from(uiDistDir) - into(serverStaticDir) - includeEmptyDirs = false -} - -tasks.named('processResources') { - if (buildUiEnabled) { - dependsOn 'syncUiStatic' - } -} - -tasks.named('bootJar') { - if (buildUiEnabled) { - dependsOn 'syncUiStatic' - } - archiveFileName = 'agentspan-runtime.jar' -} diff --git a/server/conductor-agentspan-server/build.gradle b/server/conductor-agentspan-server/build.gradle new file mode 100644 index 000000000..165684b95 --- /dev/null +++ b/server/conductor-agentspan-server/build.gradle @@ -0,0 +1,172 @@ +plugins { + id 'org.springframework.boot' + id 'java' + id 'com.vanniktech.maven.publish' +} + +def uiDir = file("${rootDir}/../ui") +def uiDistDir = file("${uiDir}/dist") +def serverStaticDir = file("${projectDir}/src/main/resources/static") +def buildUiProperty = project.findProperty('buildUI') +def buildUiEnabled = buildUiProperty != null && ( + buildUiProperty.toString().trim().isEmpty() || + buildUiProperty.toString().toBoolean() +) +def nodeOptionsWithMoreHeap = { + def current = System.getenv('NODE_OPTIONS') + current ? "${current} --max-old-space-size=4096" : '--max-old-space-size=4096' +} +def shellCommand = { String command -> + org.gradle.internal.os.OperatingSystem.current().isWindows() + ? ['cmd', '/c', command] + : ['bash', '-lc', command] +} +def pnpmCommand = { String args -> + org.gradle.internal.os.OperatingSystem.current().isWindows() + ? "where pnpm >NUL 2>NUL && pnpm ${args} || (where corepack >NUL 2>NUL && corepack pnpm ${args} || (echo pnpm or corepack is required to build the UI & exit /b 127))" + : "if command -v pnpm >/dev/null 2>&1; then pnpm ${args}; elif command -v corepack >/dev/null 2>&1; then corepack pnpm ${args}; else echo 'pnpm or corepack is required to build the UI' >&2; exit 127; fi" +} + +springBoot { + mainClass = 'dev.agentspan.runtime.AgentRuntime' + buildInfo() +} + +dependencies { + implementation project(':conductor-agentspan') + + // SPI default implementations — JDBC, crypto, and their infra deps + // (Phase 0: impls still live in conductor-agentspan; these deps are there too. + // Phase 1: impls move here, these deps become permanent server deps.) + implementation 'org.springframework:spring-jdbc' + implementation 'com.zaxxer:HikariCP' + implementation "org.xerial:sqlite-jdbc:${sqliteJdbcVersion}" + + // Conductor OSS runtime — the only place a runnable engine ships + implementation "org.conductoross:conductor-common:${conductorVersion}" + implementation "org.conductoross:conductor-core:${conductorVersion}" + implementation "org.conductoross:conductor-ai:${conductorVersion}" + implementation "org.conductoross:conductor-rest:${conductorVersion}" + implementation "org.conductoross:conductor-sqlite-persistence:${conductorVersion}" + implementation "org.conductoross:conductor-postgres-persistence:${conductorVersion}" + implementation "org.conductoross:conductor-postgres-external-storage:${conductorVersion}" + implementation "org.conductoross:conductor-scheduler-core:${conductorVersion}" + implementation "org.conductoross:conductor-scheduler-sqlite-persistence:${conductorVersion}" + implementation "org.conductoross:conductor-scheduler-postgres-persistence:${conductorVersion}" + implementation "org.conductoross:conductor-http-task:${conductorVersion}" + implementation "org.conductoross:conductor-json-jq-task:${conductorVersion}" + + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.retry:spring-retry' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + implementation 'org.springframework.boot:spring-boot-starter-log4j2' + implementation 'org.apache.logging.log4j:log4j-web' + // OpenAPI docs endpoint (configured via springdoc.* in application.properties) — a + // runtime/app concern, so it lives in the server, not the library. + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.6.0' + + compileOnly "org.projectlombok:lombok:${lombokVersion}" + annotationProcessor "org.projectlombok:lombok:${lombokVersion}" + + // Test dependencies + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}" + testImplementation "org.assertj:assertj-core:${assertjVersion}" + testImplementation "org.mockito:mockito-core:${mockitoVersion}" + testCompileOnly "org.projectlombok:lombok:${lombokVersion}" + testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" + testImplementation 'org.graalvm.polyglot:polyglot:25.0.2' + testImplementation 'org.graalvm.js:js:25.0.2' +} + +bootJar { + if (buildUiEnabled) dependsOn 'syncUiStatic' + archiveFileName = 'agentspan-runtime.jar' +} + +// Publish the plain (non-fat) jar as conductor-agentspan-server.jar (no "-plain" +// classifier). The runnable fat jar (agentspan-runtime.jar) is released separately +// via the S3/GitHub workflow, not to Maven Central. +jar { + archiveClassifier = '' +} + +mavenPublishing { + publishToMavenCentral(true) + if (project.findProperty('signingInMemoryKey') != null) { + signAllPublications() + } + + coordinates('org.conductoross', 'conductor-agentspan-server', project.version.toString()) + + pom { + name = 'Conductor AgentSpan Server' + description = 'Standalone AgentSpan server — default SPI implementations and OSS runtime over conductor-agentspan' + url = 'https://github.com/agentspan-ai/agentspan' + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/licenses/MIT' + } + } + developers { + developer { + organization = 'Orkes' + organizationUrl = 'https://orkes.io' + name = 'Orkes Development Team' + email = 'developers@orkes.io' + } + } + scm { + connection = 'scm:git:git://github.com/agentspan-ai/agentspan.git' + developerConnection = 'scm:git:ssh://github.com/agentspan-ai/agentspan.git' + url = 'https://github.com/agentspan-ai/agentspan' + } + } +} + +tasks.register('installUiDependencies', Exec) { + group = 'build' + description = 'Install UI dependencies with pnpm when the UI workspace is available' + onlyIf { uiDir.exists() } + workingDir uiDir + commandLine shellCommand(pnpmCommand('install --frozen-lockfile')) + environment 'CI', 'true' + inputs.files(file("${uiDir}/package.json"), file("${uiDir}/pnpm-lock.yaml")) + outputs.file(file("${uiDir}/node_modules/.modules.yaml")) +} + +tasks.register('buildUi', Exec) { + group = 'build' + description = 'Build the latest UI bundle for the embedded server UI (opt-in via -PbuildUI for bootJar)' + onlyIf { uiDir.exists() } + dependsOn 'installUiDependencies' + workingDir uiDir + commandLine shellCommand(pnpmCommand('build')) + environment 'CI', 'true' + environment 'NODE_OPTIONS', nodeOptionsWithMoreHeap() + inputs.files( + file("${uiDir}/package.json"), file("${uiDir}/pnpm-lock.yaml"), + file("${uiDir}/index.html"), file("${uiDir}/vite.config.ts"), + file("${uiDir}/vite-plugin-csp-nonce.ts"), file("${uiDir}/tsconfig.json") + ) + inputs.dir(file("${uiDir}/src")) + inputs.dir(file("${uiDir}/public")) + outputs.dir(uiDistDir) +} + +tasks.register('syncUiStatic', Sync) { + group = 'build' + description = 'Sync the latest built UI assets into server/src/main/resources/static' + onlyIf { uiDir.exists() } + dependsOn 'buildUi' + from(uiDistDir) + into(serverStaticDir) + includeEmptyDirs = false +} + +tasks.named('processResources') { + if (buildUiEnabled) dependsOn 'syncUiStatic' +} diff --git a/server/src/main/java/dev/agentspan/runtime/AgentRuntime.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/AgentRuntime.java similarity index 95% rename from server/src/main/java/dev/agentspan/runtime/AgentRuntime.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/AgentRuntime.java index c9511c1b4..5940cd258 100644 --- a/server/src/main/java/dev/agentspan/runtime/AgentRuntime.java +++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/AgentRuntime.java @@ -30,12 +30,9 @@ exclude = {DataSourceAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) @EnableScheduling @ComponentScan( - basePackages = { - "com.netflix.conductor", - "io.orkes.conductor", - "org.conductoross.conductor", - "dev.agentspan.runtime" - }, + // Conductor engine packages only — AgentSpan beans (dev.agentspan.runtime) are + // contributed by AgentSpanAutoConfiguration via the auto-configuration imports file. + basePackages = {"com.netflix.conductor", "io.orkes.conductor", "org.conductoross.conductor"}, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = Join.class)) @RequiredArgsConstructor public class AgentRuntime implements ApplicationRunner { diff --git a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/auth/AuthFilter.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/auth/AuthFilter.java new file mode 100644 index 000000000..4e24b03ef --- /dev/null +++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/auth/AuthFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. + */ +package dev.agentspan.runtime.auth; + +import java.io.IOException; +import java.time.Instant; +import java.util.UUID; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import dev.agentspan.runtime.context.RequestContext; +import dev.agentspan.runtime.context.RequestContextHolder; + +/** + * Populates {@link RequestContextHolder} with an anonymous principal id on every request. + * AgentSpan is single-tenant by default; an embedding application (e.g. orkes-conductor) is + * responsible for supplying its own principal adapter instead of this filter. + */ +@Component +public class AuthFilter extends OncePerRequestFilter { + + /** Matches {@code CredentialEnvSeeder.ANONYMOUS_USER_ID}. */ + static final String ANONYMOUS_USER_ID = "00000000-0000-0000-0000-000000000000"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + try { + RequestContextHolder.set(RequestContext.builder() + .requestId(UUID.randomUUID().toString()) + .userId(ANONYMOUS_USER_ID) + .createdAt(Instant.now()) + .build()); + chain.doFilter(request, response); + } finally { + RequestContextHolder.clear(); + } + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/config/CorsConfig.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/config/CorsConfig.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/config/CorsConfig.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/config/CorsConfig.java diff --git a/server/src/main/java/dev/agentspan/runtime/config/ShutdownConfig.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/config/ShutdownConfig.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/config/ShutdownConfig.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/config/ShutdownConfig.java diff --git a/server/src/main/java/dev/agentspan/runtime/config/StaticDocsConfig.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/config/StaticDocsConfig.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/config/StaticDocsConfig.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/config/StaticDocsConfig.java diff --git a/server/src/main/java/dev/agentspan/runtime/config/UiRoutingConfig.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/config/UiRoutingConfig.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/config/UiRoutingConfig.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/config/UiRoutingConfig.java diff --git a/server/src/main/java/dev/agentspan/runtime/credentials/CredentialDataSourceConfig.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialDataSourceConfig.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/credentials/CredentialDataSourceConfig.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialDataSourceConfig.java diff --git a/server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java similarity index 70% rename from server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java index cfea7764d..4a9bc395f 100644 --- a/server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java +++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java @@ -17,6 +17,8 @@ import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; +import dev.agentspan.runtime.spi.CredentialStoreProvider; + /** * On startup, seeds the credential store from well-known LLM provider environment variables. * @@ -44,65 +46,11 @@ public class CredentialEnvSeeder implements ApplicationRunner { /** * Well-known provider environment variables to scan on startup. - * Sourced from the AI provider config in application.properties plus the - * additional providers listed in the UI quick-select. * - *

    AGENTSPAN_MASTER_KEY is intentionally excluded — it is the encryption - * master key and must never be stored as a credential.

    + *

    Now sourced from the shared {@link KnownProviderEnvVars#NAMES} in the library so the + * standalone server and embedding hosts (e.g. orkes-conductor) seed an identical set.

    */ - static final List KNOWN_ENV_VARS = List.of( - // Anthropic (Claude) - "ANTHROPIC_API_KEY", - "ANTHROPIC_BASE_URL", - // OpenAI (GPT-4, DALL-E, etc.) - "OPENAI_API_KEY", - "OPENAI_ORG_ID", - "OPENAI_BASE_URL", - // Google Gemini / AI Studio / Vertex AI - "GEMINI_API_KEY", - "GOOGLE_API_KEY", - "GOOGLE_CLOUD_PROJECT", - "GOOGLE_CLOUD_LOCATION", - // Azure OpenAI - "AZURE_OPENAI_API_KEY", - "AZURE_OPENAI_ENDPOINT", - "AZURE_OPENAI_BASE_URL", - "AZURE_OPENAI_DEPLOYMENT", - // Mistral AI - "MISTRAL_API_KEY", - "MISTRAL_BASE_URL", - // Cohere - "COHERE_API_KEY", - "COHERE_BASE_URL", - // xAI / Grok - "XAI_API_KEY", - "GROK_BASE_URL", - // Groq - "GROQ_API_KEY", - // Perplexity - "PERPLEXITY_API_KEY", - "PERPLEXITY_BASE_URL", - // HuggingFace - "HUGGINGFACE_API_KEY", - "HUGGINGFACE_API_TOKEN", - // Stability AI - "STABILITY_API_KEY", - // DeepSeek - "DEEPSEEK_API_KEY", - // Together AI - "TOGETHER_API_KEY", - // Replicate - "REPLICATE_API_TOKEN", - // GitHub CLI / API - "GH_TOKEN", - "GITHUB_TOKEN", - // AWS Bedrock - "AWS_ACCESS_KEY_ID", - "AWS_SECRET_ACCESS_KEY", - "AWS_REGION", - "BEDROCK_API_KEY", - // Ollama (local inference) - "OLLAMA_HOST"); + static final List KNOWN_ENV_VARS = KnownProviderEnvVars.NAMES; private final CredentialStoreProvider storeProvider; private final Function envLookup; diff --git a/server/src/main/java/dev/agentspan/runtime/credentials/CredentialSchemaMigrator.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialSchemaMigrator.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/credentials/CredentialSchemaMigrator.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialSchemaMigrator.java diff --git a/server/src/main/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProvider.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProvider.java similarity index 99% rename from server/src/main/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProvider.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProvider.java index 65a043d34..e9b1f4cd3 100644 --- a/server/src/main/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProvider.java +++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProvider.java @@ -23,6 +23,7 @@ import org.springframework.stereotype.Component; import dev.agentspan.runtime.model.credentials.CredentialMeta; +import dev.agentspan.runtime.spi.CredentialStoreProvider; /** * AES-256-GCM encrypted credential store backed by the credential SQLite/Postgres DB. diff --git a/server/src/main/java/dev/agentspan/runtime/credentials/MasterKeyConfig.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/MasterKeyConfig.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/credentials/MasterKeyConfig.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/MasterKeyConfig.java diff --git a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/NoOpSecretOutputMasker.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/NoOpSecretOutputMasker.java new file mode 100644 index 000000000..304927246 --- /dev/null +++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/NoOpSecretOutputMasker.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. + */ +package dev.agentspan.runtime.credentials; + +import org.springframework.stereotype.Service; + +import dev.agentspan.runtime.spi.SecretOutputMasker; + +/** + * No-op {@link SecretOutputMasker} — the OSS / standalone default. + * + *

    OSS has no per-execution disclosure tracking ({@code credential_disclosures}), + * so there is nothing to redact against: {@link #mask} returns the payload unchanged. + * + *

    An embedding host (e.g. orkes-conductor) supplies a real implementation that + * queries the disclosure log, fetches the current plaintext values from the secret + * store, and redacts them from the response body via a Jackson-tree walk (so values + * containing newlines, quotes, or other JSON-escaped characters are still caught). + */ +@Service +public class NoOpSecretOutputMasker implements SecretOutputMasker { + + @Override + public String mask(String executionId, String userId, String payload) { + return payload; + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/metrics/MetricsFilterConfig.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/metrics/MetricsFilterConfig.java similarity index 100% rename from server/src/main/java/dev/agentspan/runtime/metrics/MetricsFilterConfig.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/metrics/MetricsFilterConfig.java diff --git a/server/src/main/java/dev/agentspan/runtime/service/skill/ConductorPayloadSkillPackageStore.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/ConductorPayloadSkillPackageStore.java similarity index 97% rename from server/src/main/java/dev/agentspan/runtime/service/skill/ConductorPayloadSkillPackageStore.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/ConductorPayloadSkillPackageStore.java index 42081e522..77174d44b 100644 --- a/server/src/main/java/dev/agentspan/runtime/service/skill/ConductorPayloadSkillPackageStore.java +++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/ConductorPayloadSkillPackageStore.java @@ -20,6 +20,9 @@ import com.netflix.conductor.common.utils.ExternalPayloadStorage.Operation; import com.netflix.conductor.common.utils.ExternalPayloadStorage.PayloadType; +import dev.agentspan.runtime.spi.SkillPackageStore; +import dev.agentspan.runtime.spi.StoredSkillPackage; + @Component @ConditionalOnProperty(prefix = "agentspan.skills.package-store", name = "type", havingValue = "conductor-payload") public class ConductorPayloadSkillPackageStore implements SkillPackageStore { diff --git a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java new file mode 100644 index 000000000..711dce57c --- /dev/null +++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillMetadataDAO.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2025 AgentSpan + * Licensed under the MIT License. See LICENSE file in the project root for details. + */ + +package dev.agentspan.runtime.service.skill; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import dev.agentspan.runtime.model.skill.SkillDetail; +import dev.agentspan.runtime.spi.SkillMetadataDAO; + +/** + * Default {@link SkillMetadataDAO} — stores skill metadata as {@code metadata.json} files on the + * local filesystem under {@code /owners////}, with a per-skill + * {@code latest} pointer file. This is the standalone-server default; it preserves the exact + * on-disk layout used before the SPI extraction. Embedding hosts (e.g. orkes-conductor) supply a + * durable/HA implementation instead (this class ships only in {@code conductor-agentspan-server}, + * so it is never on an embedding host's classpath). + */ +@Component +public class FileSystemSkillMetadataDAO implements SkillMetadataDAO { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final Path storageRoot; + + public FileSystemSkillMetadataDAO( + @Value("${agentspan.skills.storage.directory:${java.io.tmpdir}/agentspan/skills}") String storageDir) { + this.storageRoot = Path.of(storageDir).toAbsolutePath().normalize(); + } + + @Override + public void save(SkillDetail detail, boolean makeLatest) { + Path metadataPath = metadataPath(detail.getOwnerId(), detail.getName(), detail.getVersion()); + try { + Files.createDirectories(metadataPath.getParent()); + writeDetail(metadataPath, detail); + if (makeLatest) { + Files.writeString( + latestPath(detail.getOwnerId(), detail.getName()), detail.getVersion(), StandardCharsets.UTF_8); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to write skill metadata: " + e.getMessage(), e); + } + } + + @Override + public Optional find(String ownerId, String name, String version) { + Path metadataPath = metadataPath(ownerId, name, version); + if (!Files.exists(metadataPath)) { + return Optional.empty(); + } + return Optional.of(readDetail(metadataPath)); + } + + @Override + public Optional latestVersion(String ownerId, String name) { + Path latest = latestPath(ownerId, name); + if (!Files.exists(latest)) { + return Optional.empty(); + } + try { + return Optional.of(Files.readString(latest, StandardCharsets.UTF_8).trim()); + } catch (IOException e) { + throw new IllegalStateException("Failed to read latest skill version: " + e.getMessage(), e); + } + } + + @Override + public List listVersions(String ownerId, String name) { + Path skillRoot = skillRoot(ownerId, name); + List details = new ArrayList<>(); + if (!Files.isDirectory(skillRoot)) { + return details; + } + try (var versions = Files.list(skillRoot)) { + for (Path versionPath : versions.filter(Files::isDirectory).toList()) { + Path metadata = versionPath.resolve("metadata.json"); + if (Files.exists(metadata)) { + details.add(readDetail(metadata)); + } + } + } catch (IOException e) { + throw new IllegalStateException("Failed to list skill versions: " + e.getMessage(), e); + } + return details; + } + + @Override + public List list(String ownerId, boolean allVersions) { + Path ownerRoot = ownerRoot(ownerId); + List details = new ArrayList<>(); + if (!Files.isDirectory(ownerRoot)) { + return details; + } + try (var skillDirs = Files.list(ownerRoot)) { + for (Path skillDir : skillDirs.filter(Files::isDirectory).toList()) { + if (allVersions) { + try (var versions = Files.list(skillDir)) { + for (Path versionPath : + versions.filter(Files::isDirectory).toList()) { + Path metadata = versionPath.resolve("metadata.json"); + if (Files.exists(metadata)) { + details.add(readDetail(metadata)); + } + } + } + } else { + Path latest = skillDir.resolve("latest"); + if (Files.exists(latest)) { + String version = + Files.readString(latest, StandardCharsets.UTF_8).trim(); + Path metadata = skillDir.resolve(encoded(version)).resolve("metadata.json"); + if (Files.exists(metadata)) { + details.add(readDetail(metadata)); + } + } + } + } + } catch (IOException e) { + throw new IllegalStateException("Failed to list skills: " + e.getMessage(), e); + } + return details; + } + + @Override + public void delete(String ownerId, String name, String version) { + Path dir = versionDir(ownerId, name, version); + if (!Files.exists(dir)) { + return; + } + try (var paths = Files.walk(dir)) { + for (Path p : paths.sorted(Comparator.reverseOrder()).toList()) { + Files.deleteIfExists(p); + } + Path latest = latestPath(ownerId, name); + if (Files.exists(latest) + && version.equals( + Files.readString(latest, StandardCharsets.UTF_8).trim())) { + updateLatestAfterDelete(ownerId, name); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to delete skill metadata: " + e.getMessage(), e); + } + } + + private void updateLatestAfterDelete(String ownerId, String name) throws IOException { + Path skillRoot = skillRoot(ownerId, name); + if (!Files.isDirectory(skillRoot)) { + Files.deleteIfExists(latestPath(ownerId, name)); + return; + } + List remaining = listVersions(ownerId, name); + if (remaining.isEmpty()) { + Files.deleteIfExists(latestPath(ownerId, name)); + try (var children = Files.list(skillRoot)) { + if (children.findAny().isEmpty()) { + Files.deleteIfExists(skillRoot); + } + } + return; + } + remaining.sort(Comparator.comparing(SkillDetail::getCreatedAt, Comparator.nullsFirst(Long::compareTo)) + .thenComparing(SkillDetail::getVersion)); + Files.writeString( + latestPath(ownerId, name), remaining.get(remaining.size() - 1).getVersion(), StandardCharsets.UTF_8); + } + + private SkillDetail readDetail(Path metadataPath) { + try { + return MAPPER.readValue(metadataPath.toFile(), SkillDetail.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to read skill metadata: " + e.getMessage(), e); + } + } + + private void writeDetail(Path metadataPath, SkillDetail detail) { + try { + MAPPER.writerWithDefaultPrettyPrinter().writeValue(metadataPath.toFile(), detail); + } catch (IOException e) { + throw new IllegalStateException("Failed to write skill metadata: " + e.getMessage(), e); + } + } + + private Path ownerRoot(String ownerId) { + return storageRoot.resolve("owners").resolve(encoded(ownerId)); + } + + private Path skillRoot(String ownerId, String name) { + return ownerRoot(ownerId).resolve(encoded(name)); + } + + private Path versionDir(String ownerId, String name, String version) { + return skillRoot(ownerId, name).resolve(encoded(version)); + } + + private Path metadataPath(String ownerId, String name, String version) { + return versionDir(ownerId, name, version).resolve("metadata.json"); + } + + private Path latestPath(String ownerId, String name) { + return skillRoot(ownerId, name).resolve("latest"); + } + + private String encoded(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } +} diff --git a/server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillPackageStore.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillPackageStore.java similarity index 97% rename from server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillPackageStore.java rename to server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillPackageStore.java index 396ee24cd..f1964be54 100644 --- a/server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillPackageStore.java +++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/service/skill/FileSystemSkillPackageStore.java @@ -17,6 +17,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; +import dev.agentspan.runtime.spi.SkillPackageStore; +import dev.agentspan.runtime.spi.StoredSkillPackage; + @Component @ConditionalOnProperty( prefix = "agentspan.skills.package-store", diff --git a/server/src/main/resources/application-postgres.properties b/server/conductor-agentspan-server/src/main/resources/application-postgres.properties similarity index 100% rename from server/src/main/resources/application-postgres.properties rename to server/conductor-agentspan-server/src/main/resources/application-postgres.properties diff --git a/server/src/main/resources/application-rag.properties b/server/conductor-agentspan-server/src/main/resources/application-rag.properties similarity index 100% rename from server/src/main/resources/application-rag.properties rename to server/conductor-agentspan-server/src/main/resources/application-rag.properties diff --git a/server/src/main/resources/application.properties b/server/conductor-agentspan-server/src/main/resources/application.properties similarity index 95% rename from server/src/main/resources/application.properties rename to server/conductor-agentspan-server/src/main/resources/application.properties index 83d9e1f6a..4e227def7 100644 --- a/server/src/main/resources/application.properties +++ b/server/conductor-agentspan-server/src/main/resources/application.properties @@ -148,15 +148,6 @@ agentspan.skills.max-preview-bytes=${AGENTSPAN_SKILLS_MAX_PREVIEW_BYTES:1048576} agentspan.skills.max-uncompressed-bytes=${AGENTSPAN_SKILLS_MAX_UNCOMPRESSED_BYTES:209715200} agentspan.skills.max-file-count=${AGENTSPAN_SKILLS_MAX_FILE_COUNT:2000} -# ============================================================================= -# Auth Configuration -# ============================================================================= -agentspan.auth.enabled=false - -# Default users (bcrypt passwords — plain text here are hashed at startup) -agentspan.auth.users[0].username=agentspan -agentspan.auth.users[0].password=agentspan - # ============================================================================= # Credential Store Configuration # ============================================================================= diff --git a/server/src/main/resources/banner.txt b/server/conductor-agentspan-server/src/main/resources/banner.txt similarity index 100% rename from server/src/main/resources/banner.txt rename to server/conductor-agentspan-server/src/main/resources/banner.txt diff --git a/server/src/main/resources/log4j2.xml b/server/conductor-agentspan-server/src/main/resources/log4j2.xml similarity index 100% rename from server/src/main/resources/log4j2.xml rename to server/conductor-agentspan-server/src/main/resources/log4j2.xml diff --git a/server/src/main/resources/schema-credentials-postgres.sql b/server/conductor-agentspan-server/src/main/resources/schema-credentials-postgres.sql similarity index 70% rename from server/src/main/resources/schema-credentials-postgres.sql rename to server/conductor-agentspan-server/src/main/resources/schema-credentials-postgres.sql index 9b478d84a..301992422 100644 --- a/server/src/main/resources/schema-credentials-postgres.sql +++ b/server/conductor-agentspan-server/src/main/resources/schema-credentials-postgres.sql @@ -12,24 +12,6 @@ DROP TABLE IF EXISTS credentials_binding; -CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - email TEXT, - username TEXT NOT NULL UNIQUE, - password_hash TEXT, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS api_keys ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - key_hash TEXT NOT NULL UNIQUE, - label TEXT, - last_used_at TEXT, - created_at TEXT NOT NULL -); - CREATE TABLE IF NOT EXISTS credentials_store ( user_id TEXT NOT NULL, name TEXT NOT NULL, diff --git a/server/src/main/resources/schema-credentials.sql b/server/conductor-agentspan-server/src/main/resources/schema-credentials.sql similarity index 62% rename from server/src/main/resources/schema-credentials.sql rename to server/conductor-agentspan-server/src/main/resources/schema-credentials.sql index 3a29a8cf7..2f320cb80 100644 --- a/server/src/main/resources/schema-credentials.sql +++ b/server/conductor-agentspan-server/src/main/resources/schema-credentials.sql @@ -10,24 +10,6 @@ -- lookup for Conductor-parity secrets API). Safe to re-run. DROP TABLE IF EXISTS credentials_binding; -CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, -- UUID as string - name TEXT NOT NULL, - email TEXT, - username TEXT NOT NULL UNIQUE, - password_hash TEXT, -- bcrypt; NULL for API-key-only users - created_at TEXT NOT NULL -- ISO-8601 UTC -); - -CREATE TABLE IF NOT EXISTS api_keys ( - id TEXT PRIMARY KEY, -- UUID as string - user_id TEXT NOT NULL, - key_hash TEXT NOT NULL UNIQUE, -- SHA-256 hex of raw key - label TEXT, - last_used_at TEXT, -- ISO-8601 UTC, updated on use - created_at TEXT NOT NULL -); - CREATE TABLE IF NOT EXISTS credentials_store ( user_id TEXT NOT NULL, name TEXT NOT NULL, diff --git a/server/src/main/resources/static/agentspan-icon.svg b/server/conductor-agentspan-server/src/main/resources/static/agentspan-icon.svg similarity index 100% rename from server/src/main/resources/static/agentspan-icon.svg rename to server/conductor-agentspan-server/src/main/resources/static/agentspan-icon.svg diff --git a/server/src/main/resources/static/agentspan-logo-dark.svg b/server/conductor-agentspan-server/src/main/resources/static/agentspan-logo-dark.svg similarity index 100% rename from server/src/main/resources/static/agentspan-logo-dark.svg rename to server/conductor-agentspan-server/src/main/resources/static/agentspan-logo-dark.svg diff --git a/server/src/main/resources/static/agentspan-logo-light.svg b/server/conductor-agentspan-server/src/main/resources/static/agentspan-logo-light.svg similarity index 100% rename from server/src/main/resources/static/agentspan-logo-light.svg rename to server/conductor-agentspan-server/src/main/resources/static/agentspan-logo-light.svg diff --git a/server/src/main/resources/static/agentspan-logo-small.svg b/server/conductor-agentspan-server/src/main/resources/static/agentspan-logo-small.svg similarity index 100% rename from server/src/main/resources/static/agentspan-logo-small.svg rename to server/conductor-agentspan-server/src/main/resources/static/agentspan-logo-small.svg diff --git a/server/src/main/resources/static/agentspan-logo.svg b/server/conductor-agentspan-server/src/main/resources/static/agentspan-logo.svg similarity index 100% rename from server/src/main/resources/static/agentspan-logo.svg rename to server/conductor-agentspan-server/src/main/resources/static/agentspan-logo.svg diff --git a/server/src/main/resources/static/assets/DailyMotion-Bik8u2gQ.js b/server/conductor-agentspan-server/src/main/resources/static/assets/DailyMotion-DuBKwtXw.js similarity index 97% rename from server/src/main/resources/static/assets/DailyMotion-Bik8u2gQ.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/DailyMotion-DuBKwtXw.js index 96f4b236c..66e24a0a1 100644 --- a/server/src/main/resources/static/assets/DailyMotion-Bik8u2gQ.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/DailyMotion-DuBKwtXw.js @@ -1 +1 @@ -import{r as N,b as x,d as R,g as q}from"./index-DiybVIaQ.js";function K(l,a){for(var p=0;pn[s]})}}}return Object.freeze(Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}))}var f,D;function B(){if(D)return f;D=1;var l=Object.create,a=Object.defineProperty,p=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,s=Object.getPrototypeOf,u=Object.prototype.hasOwnProperty,b=(t,e,r)=>e in t?a(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,M=(t,e)=>{for(var r in e)a(t,r,{get:e[r],enumerable:!0})},g=(t,e,r,h)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of n(e))!u.call(t,i)&&i!==r&&a(t,i,{get:()=>e[i],enumerable:!(h=p(e,i))||h.enumerable});return t},w=(t,e,r)=>(r=t!=null?l(s(t)):{},g(!t||!t.__esModule?a(r,"default",{value:t,enumerable:!0}):r,t)),S=t=>g(a({},"__esModule",{value:!0}),t),o=(t,e,r)=>(b(t,typeof e!="symbol"?e+"":e,r),r),m={};M(m,{default:()=>y}),f=S(m);var d=w(N()),c=x(),P=R();const j="https://api.dmcdn.net/all.js",T="DM",E="dmAsyncInit";class y extends d.Component{constructor(){super(...arguments),o(this,"callPlayer",c.callPlayer),o(this,"onDurationChange",()=>{const e=this.getDuration();this.props.onDuration(e)}),o(this,"mute",()=>{this.callPlayer("setMuted",!0)}),o(this,"unmute",()=>{this.callPlayer("setMuted",!1)}),o(this,"ref",e=>{this.container=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){const{controls:r,config:h,onError:i,playing:A}=this.props,[,v]=e.match(P.MATCH_URL_DAILYMOTION);if(this.player){this.player.load(v,{start:(0,c.parseStartTime)(e),autoplay:A});return}(0,c.getSDK)(j,T,E,_=>_.player).then(_=>{if(!this.container)return;const L=_.player;this.player=new L(this.container,{width:"100%",height:"100%",video:v,params:{controls:r,autoplay:this.props.playing,mute:this.props.muted,start:(0,c.parseStartTime)(e),origin:window.location.origin,...h.params},events:{apiready:this.props.onReady,seeked:()=>this.props.onSeek(this.player.currentTime),video_end:this.props.onEnded,durationchange:this.onDurationChange,pause:this.props.onPause,playing:this.props.onPlay,waiting:this.props.onBuffer,error:C=>i(C)}})},i)}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){}seekTo(e,r=!0){this.callPlayer("seek",e),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e)}getDuration(){return this.player.duration||null}getCurrentTime(){return this.player.currentTime}getSecondsLoaded(){return this.player.bufferedTime}render(){const{display:e}=this.props,r={width:"100%",height:"100%",display:e};return d.default.createElement("div",{style:r},d.default.createElement("div",{ref:this.ref}))}}return o(y,"displayName","DailyMotion"),o(y,"canPlay",P.canPlay.dailymotion),o(y,"loopOnEnded",!0),f}var O=B();const I=q(O),k=K({__proto__:null,default:I},[O]);export{k as D}; +import{r as N,b as x,d as R,g as q}from"./index-DVa6sjDi.js";function K(l,a){for(var p=0;pn[s]})}}}return Object.freeze(Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}))}var f,D;function B(){if(D)return f;D=1;var l=Object.create,a=Object.defineProperty,p=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,s=Object.getPrototypeOf,u=Object.prototype.hasOwnProperty,b=(t,e,r)=>e in t?a(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,M=(t,e)=>{for(var r in e)a(t,r,{get:e[r],enumerable:!0})},g=(t,e,r,h)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of n(e))!u.call(t,i)&&i!==r&&a(t,i,{get:()=>e[i],enumerable:!(h=p(e,i))||h.enumerable});return t},w=(t,e,r)=>(r=t!=null?l(s(t)):{},g(!t||!t.__esModule?a(r,"default",{value:t,enumerable:!0}):r,t)),S=t=>g(a({},"__esModule",{value:!0}),t),o=(t,e,r)=>(b(t,typeof e!="symbol"?e+"":e,r),r),m={};M(m,{default:()=>y}),f=S(m);var d=w(N()),c=x(),P=R();const j="https://api.dmcdn.net/all.js",T="DM",E="dmAsyncInit";class y extends d.Component{constructor(){super(...arguments),o(this,"callPlayer",c.callPlayer),o(this,"onDurationChange",()=>{const e=this.getDuration();this.props.onDuration(e)}),o(this,"mute",()=>{this.callPlayer("setMuted",!0)}),o(this,"unmute",()=>{this.callPlayer("setMuted",!1)}),o(this,"ref",e=>{this.container=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){const{controls:r,config:h,onError:i,playing:A}=this.props,[,v]=e.match(P.MATCH_URL_DAILYMOTION);if(this.player){this.player.load(v,{start:(0,c.parseStartTime)(e),autoplay:A});return}(0,c.getSDK)(j,T,E,_=>_.player).then(_=>{if(!this.container)return;const L=_.player;this.player=new L(this.container,{width:"100%",height:"100%",video:v,params:{controls:r,autoplay:this.props.playing,mute:this.props.muted,start:(0,c.parseStartTime)(e),origin:window.location.origin,...h.params},events:{apiready:this.props.onReady,seeked:()=>this.props.onSeek(this.player.currentTime),video_end:this.props.onEnded,durationchange:this.onDurationChange,pause:this.props.onPause,playing:this.props.onPlay,waiting:this.props.onBuffer,error:C=>i(C)}})},i)}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){}seekTo(e,r=!0){this.callPlayer("seek",e),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e)}getDuration(){return this.player.duration||null}getCurrentTime(){return this.player.currentTime}getSecondsLoaded(){return this.player.bufferedTime}render(){const{display:e}=this.props,r={width:"100%",height:"100%",display:e};return d.default.createElement("div",{style:r},d.default.createElement("div",{ref:this.ref}))}}return o(y,"displayName","DailyMotion"),o(y,"canPlay",P.canPlay.dailymotion),o(y,"loopOnEnded",!0),f}var O=B();const I=q(O),k=K({__proto__:null,default:I},[O]);export{k as D}; diff --git a/server/src/main/resources/static/assets/Facebook-DL8ElG-z.js b/server/conductor-agentspan-server/src/main/resources/static/assets/Facebook-D-v5cNby.js similarity index 98% rename from server/src/main/resources/static/assets/Facebook-DL8ElG-z.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/Facebook-D-v5cNby.js index 2c9d9d027..e633f6fa0 100644 --- a/server/src/main/resources/static/assets/Facebook-DL8ElG-z.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/Facebook-D-v5cNby.js @@ -1 +1 @@ -import{r as I,b as w,d as x,g as L}from"./index-DiybVIaQ.js";function M(p,a){for(var u=0;ui[n]})}}}return Object.freeze(Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}))}var b,v;function A(){if(v)return b;v=1;var p=Object.create,a=Object.defineProperty,u=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,n=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,D=(t,e,r)=>e in t?a(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,E=(t,e)=>{for(var r in e)a(t,r,{get:e[r],enumerable:!0})},h=(t,e,r,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of i(e))!c.call(t,s)&&s!==r&&a(t,s,{get:()=>e[s],enumerable:!(o=u(e,s))||o.enumerable});return t},k=(t,e,r)=>(r=t!=null?p(n(t)):{},h(!t||!t.__esModule?a(r,"default",{value:t,enumerable:!0}):r,t)),F=t=>h(a({},"__esModule",{value:!0}),t),l=(t,e,r)=>(D(t,typeof e!="symbol"?e+"":e,r),r),d={};E(d,{default:()=>y}),b=F(d);var _=k(I()),f=w(),S=x();const P="https://connect.facebook.net/en_US/sdk.js",g="FB",m="fbAsyncInit",j="facebook-player-";class y extends _.Component{constructor(){super(...arguments),l(this,"callPlayer",f.callPlayer),l(this,"playerID",this.props.config.playerId||`${j}${(0,f.randomString)()}`),l(this,"mute",()=>{this.callPlayer("mute")}),l(this,"unmute",()=>{this.callPlayer("unmute")})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e,r){if(r){(0,f.getSDK)(P,g,m).then(o=>o.XFBML.parse());return}(0,f.getSDK)(P,g,m).then(o=>{o.init({appId:this.props.config.appId,xfbml:!0,version:this.props.config.version}),o.Event.subscribe("xfbml.render",s=>{this.props.onLoaded()}),o.Event.subscribe("xfbml.ready",s=>{s.type==="video"&&s.id===this.playerID&&(this.player=s.instance,this.player.subscribe("startedPlaying",this.props.onPlay),this.player.subscribe("paused",this.props.onPause),this.player.subscribe("finishedPlaying",this.props.onEnded),this.player.subscribe("startedBuffering",this.props.onBuffer),this.player.subscribe("finishedBuffering",this.props.onBufferEnd),this.player.subscribe("error",this.props.onError),this.props.muted?this.callPlayer("mute"):this.callPlayer("unmute"),this.props.onReady(),document.getElementById(this.playerID).querySelector("iframe").style.visibility="visible")})})}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){}seekTo(e,r=!0){this.callPlayer("seek",e),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e)}getDuration(){return this.callPlayer("getDuration")}getCurrentTime(){return this.callPlayer("getCurrentPosition")}getSecondsLoaded(){return null}render(){const{attributes:e}=this.props.config,r={width:"100%",height:"100%"};return _.default.createElement("div",{style:r,id:this.playerID,className:"fb-video","data-href":this.props.url,"data-autoplay":this.props.playing?"true":"false","data-allowfullscreen":"true","data-controls":this.props.controls?"true":"false",...e})}}return l(y,"displayName","Facebook"),l(y,"canPlay",S.canPlay.facebook),l(y,"loopOnEnded",!0),b}var O=A();const B=L(O),R=M({__proto__:null,default:B},[O]);export{R as F}; +import{r as I,b as w,d as x,g as L}from"./index-DVa6sjDi.js";function M(p,a){for(var u=0;ui[n]})}}}return Object.freeze(Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}))}var b,v;function A(){if(v)return b;v=1;var p=Object.create,a=Object.defineProperty,u=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,n=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,D=(t,e,r)=>e in t?a(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,E=(t,e)=>{for(var r in e)a(t,r,{get:e[r],enumerable:!0})},h=(t,e,r,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of i(e))!c.call(t,s)&&s!==r&&a(t,s,{get:()=>e[s],enumerable:!(o=u(e,s))||o.enumerable});return t},k=(t,e,r)=>(r=t!=null?p(n(t)):{},h(!t||!t.__esModule?a(r,"default",{value:t,enumerable:!0}):r,t)),F=t=>h(a({},"__esModule",{value:!0}),t),l=(t,e,r)=>(D(t,typeof e!="symbol"?e+"":e,r),r),d={};E(d,{default:()=>y}),b=F(d);var _=k(I()),f=w(),S=x();const P="https://connect.facebook.net/en_US/sdk.js",g="FB",m="fbAsyncInit",j="facebook-player-";class y extends _.Component{constructor(){super(...arguments),l(this,"callPlayer",f.callPlayer),l(this,"playerID",this.props.config.playerId||`${j}${(0,f.randomString)()}`),l(this,"mute",()=>{this.callPlayer("mute")}),l(this,"unmute",()=>{this.callPlayer("unmute")})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e,r){if(r){(0,f.getSDK)(P,g,m).then(o=>o.XFBML.parse());return}(0,f.getSDK)(P,g,m).then(o=>{o.init({appId:this.props.config.appId,xfbml:!0,version:this.props.config.version}),o.Event.subscribe("xfbml.render",s=>{this.props.onLoaded()}),o.Event.subscribe("xfbml.ready",s=>{s.type==="video"&&s.id===this.playerID&&(this.player=s.instance,this.player.subscribe("startedPlaying",this.props.onPlay),this.player.subscribe("paused",this.props.onPause),this.player.subscribe("finishedPlaying",this.props.onEnded),this.player.subscribe("startedBuffering",this.props.onBuffer),this.player.subscribe("finishedBuffering",this.props.onBufferEnd),this.player.subscribe("error",this.props.onError),this.props.muted?this.callPlayer("mute"):this.callPlayer("unmute"),this.props.onReady(),document.getElementById(this.playerID).querySelector("iframe").style.visibility="visible")})})}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){}seekTo(e,r=!0){this.callPlayer("seek",e),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e)}getDuration(){return this.callPlayer("getDuration")}getCurrentTime(){return this.callPlayer("getCurrentPosition")}getSecondsLoaded(){return null}render(){const{attributes:e}=this.props.config,r={width:"100%",height:"100%"};return _.default.createElement("div",{style:r,id:this.playerID,className:"fb-video","data-href":this.props.url,"data-autoplay":this.props.playing?"true":"false","data-allowfullscreen":"true","data-controls":this.props.controls?"true":"false",...e})}}return l(y,"displayName","Facebook"),l(y,"canPlay",S.canPlay.facebook),l(y,"loopOnEnded",!0),b}var O=A();const B=L(O),R=M({__proto__:null,default:B},[O]);export{R as F}; diff --git a/server/src/main/resources/static/assets/FilePlayer-C3WXHWsE.js b/server/conductor-agentspan-server/src/main/resources/static/assets/FilePlayer-BGKWD3yK.js similarity index 99% rename from server/src/main/resources/static/assets/FilePlayer-C3WXHWsE.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/FilePlayer-BGKWD3yK.js index e66c5ec00..bd6db5463 100644 --- a/server/src/main/resources/static/assets/FilePlayer-C3WXHWsE.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/FilePlayer-BGKWD3yK.js @@ -1 +1 @@ -import{r as q,b as G,d as X,g as W}from"./index-DiybVIaQ.js";function z(u,h){for(var f=0;fp[d]})}}}return Object.freeze(Object.defineProperty(u,Symbol.toStringTag,{value:"Module"}))}var S,A;function J(){if(A)return S;A=1;var u=Object.create,h=Object.defineProperty,f=Object.getOwnPropertyDescriptor,p=Object.getOwnPropertyNames,d=Object.getPrototypeOf,y=Object.prototype.hasOwnProperty,I=(s,e,t)=>e in s?h(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t,D=(s,e)=>{for(var t in e)h(s,t,{get:e[t],enumerable:!0})},b=(s,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of p(e))!y.call(s,o)&&o!==t&&h(s,o,{get:()=>e[o],enumerable:!(r=f(e,o))||r.enumerable});return s},w=(s,e,t)=>(t=s!=null?u(d(s)):{},b(!s||!s.__esModule?h(t,"default",{value:s,enumerable:!0}):t,s)),M=s=>b(h({},"__esModule",{value:!0}),s),i=(s,e,t)=>(I(s,typeof e!="symbol"?e+"":e,t),t),_={};D(_,{default:()=>g}),S=M(_);var P=w(q()),a=G(),v=X();const E=typeof navigator<"u",k=E&&navigator.platform==="MacIntel"&&navigator.maxTouchPoints>1,O=E&&(/iPad|iPhone|iPod/.test(navigator.userAgent)||k)&&!window.MSStream,U=E&&/^((?!chrome|android).)*safari/i.test(navigator.userAgent)&&!window.MSStream,F="https://cdn.jsdelivr.net/npm/hls.js@VERSION/dist/hls.min.js",N="Hls",j="https://cdnjs.cloudflare.com/ajax/libs/dashjs/VERSION/dash.all.min.js",H="dashjs",V="https://cdn.jsdelivr.net/npm/flv.js@VERSION/dist/flv.min.js",T="flvjs",C=/www\.dropbox\.com\/.+/,m=/https:\/\/watch\.cloudflarestream\.com\/([a-z0-9]+)/,B="https://videodelivery.net/{id}/manifest/video.m3u8";class g extends P.Component{constructor(){super(...arguments),i(this,"onReady",(...e)=>this.props.onReady(...e)),i(this,"onPlay",(...e)=>this.props.onPlay(...e)),i(this,"onBuffer",(...e)=>this.props.onBuffer(...e)),i(this,"onBufferEnd",(...e)=>this.props.onBufferEnd(...e)),i(this,"onPause",(...e)=>this.props.onPause(...e)),i(this,"onEnded",(...e)=>this.props.onEnded(...e)),i(this,"onError",(...e)=>this.props.onError(...e)),i(this,"onPlayBackRateChange",e=>this.props.onPlaybackRateChange(e.target.playbackRate)),i(this,"onEnablePIP",(...e)=>this.props.onEnablePIP(...e)),i(this,"onDisablePIP",e=>{const{onDisablePIP:t,playing:r}=this.props;t(e),r&&this.play()}),i(this,"onPresentationModeChange",e=>{if(this.player&&(0,a.supportsWebKitPresentationMode)(this.player)){const{webkitPresentationMode:t}=this.player;t==="picture-in-picture"?this.onEnablePIP(e):t==="inline"&&this.onDisablePIP(e)}}),i(this,"onSeek",e=>{this.props.onSeek(e.target.currentTime)}),i(this,"mute",()=>{this.player.muted=!0}),i(this,"unmute",()=>{this.player.muted=!1}),i(this,"renderSourceElement",(e,t)=>typeof e=="string"?P.default.createElement("source",{key:t,src:e}):P.default.createElement("source",{key:t,...e})),i(this,"renderTrack",(e,t)=>P.default.createElement("track",{key:t,...e})),i(this,"ref",e=>{this.player&&(this.prevPlayer=this.player),this.player=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this),this.addListeners(this.player);const e=this.getSource(this.props.url);e&&(this.player.src=e),(O||this.props.config.forceDisableHls)&&this.player.load()}componentDidUpdate(e){this.shouldUseAudio(this.props)!==this.shouldUseAudio(e)&&(this.removeListeners(this.prevPlayer,e.url),this.addListeners(this.player)),this.props.url!==e.url&&!(0,a.isMediaStream)(this.props.url)&&!(this.props.url instanceof Array)&&(this.player.srcObject=null)}componentWillUnmount(){this.player.removeAttribute("src"),this.removeListeners(this.player),this.hls&&this.hls.destroy()}addListeners(e){const{url:t,playsinline:r}=this.props;e.addEventListener("play",this.onPlay),e.addEventListener("waiting",this.onBuffer),e.addEventListener("playing",this.onBufferEnd),e.addEventListener("pause",this.onPause),e.addEventListener("seeked",this.onSeek),e.addEventListener("ended",this.onEnded),e.addEventListener("error",this.onError),e.addEventListener("ratechange",this.onPlayBackRateChange),e.addEventListener("enterpictureinpicture",this.onEnablePIP),e.addEventListener("leavepictureinpicture",this.onDisablePIP),e.addEventListener("webkitpresentationmodechanged",this.onPresentationModeChange),this.shouldUseHLS(t)||e.addEventListener("canplay",this.onReady),r&&(e.setAttribute("playsinline",""),e.setAttribute("webkit-playsinline",""),e.setAttribute("x5-playsinline",""))}removeListeners(e,t){e.removeEventListener("canplay",this.onReady),e.removeEventListener("play",this.onPlay),e.removeEventListener("waiting",this.onBuffer),e.removeEventListener("playing",this.onBufferEnd),e.removeEventListener("pause",this.onPause),e.removeEventListener("seeked",this.onSeek),e.removeEventListener("ended",this.onEnded),e.removeEventListener("error",this.onError),e.removeEventListener("ratechange",this.onPlayBackRateChange),e.removeEventListener("enterpictureinpicture",this.onEnablePIP),e.removeEventListener("leavepictureinpicture",this.onDisablePIP),e.removeEventListener("webkitpresentationmodechanged",this.onPresentationModeChange),this.shouldUseHLS(t)||e.removeEventListener("canplay",this.onReady)}shouldUseAudio(e){return e.config.forceVideo||e.config.attributes.poster?!1:v.AUDIO_EXTENSIONS.test(e.url)||e.config.forceAudio}shouldUseHLS(e){return U&&this.props.config.forceSafariHLS||this.props.config.forceHLS?!0:O||this.props.config.forceDisableHls?!1:v.HLS_EXTENSIONS.test(e)||m.test(e)}shouldUseDASH(e){return v.DASH_EXTENSIONS.test(e)||this.props.config.forceDASH}shouldUseFLV(e){return v.FLV_EXTENSIONS.test(e)||this.props.config.forceFLV}load(e){const{hlsVersion:t,hlsOptions:r,dashVersion:o,flvVersion:L}=this.props.config;if(this.hls&&this.hls.destroy(),this.dash&&this.dash.reset(),this.shouldUseHLS(e)&&(0,a.getSDK)(F.replace("VERSION",t),N).then(n=>{if(this.hls=new n(r),this.hls.on(n.Events.MANIFEST_PARSED,()=>{this.props.onReady()}),this.hls.on(n.Events.ERROR,(l,c)=>{this.props.onError(l,c,this.hls,n)}),m.test(e)){const l=e.match(m)[1];this.hls.loadSource(B.replace("{id}",l))}else this.hls.loadSource(e);this.hls.attachMedia(this.player),this.props.onLoaded()}),this.shouldUseDASH(e)&&(0,a.getSDK)(j.replace("VERSION",o),H).then(n=>{this.dash=n.MediaPlayer().create(),this.dash.initialize(this.player,e,this.props.playing),this.dash.on("error",this.props.onError),parseInt(o)<3?this.dash.getDebug().setLogToBrowserConsole(!1):this.dash.updateSettings({debug:{logLevel:n.Debug.LOG_LEVEL_NONE}}),this.props.onLoaded()}),this.shouldUseFLV(e)&&(0,a.getSDK)(V.replace("VERSION",L),T).then(n=>{this.flv=n.createPlayer({type:"flv",url:e}),this.flv.attachMediaElement(this.player),this.flv.on(n.Events.ERROR,(l,c)=>{this.props.onError(l,c,this.flv,n)}),this.flv.load(),this.props.onLoaded()}),e instanceof Array)this.player.load();else if((0,a.isMediaStream)(e))try{this.player.srcObject=e}catch{this.player.src=window.URL.createObjectURL(e)}}play(){const e=this.player.play();e&&e.catch(this.props.onError)}pause(){this.player.pause()}stop(){this.player.removeAttribute("src"),this.dash&&this.dash.reset()}seekTo(e,t=!0){this.player.currentTime=e,t||this.pause()}setVolume(e){this.player.volume=e}enablePIP(){this.player.requestPictureInPicture&&document.pictureInPictureElement!==this.player?this.player.requestPictureInPicture():(0,a.supportsWebKitPresentationMode)(this.player)&&this.player.webkitPresentationMode!=="picture-in-picture"&&this.player.webkitSetPresentationMode("picture-in-picture")}disablePIP(){document.exitPictureInPicture&&document.pictureInPictureElement===this.player?document.exitPictureInPicture():(0,a.supportsWebKitPresentationMode)(this.player)&&this.player.webkitPresentationMode!=="inline"&&this.player.webkitSetPresentationMode("inline")}setPlaybackRate(e){try{this.player.playbackRate=e}catch(t){this.props.onError(t)}}getDuration(){if(!this.player)return null;const{duration:e,seekable:t}=this.player;return e===1/0&&t.length>0?t.end(t.length-1):e}getCurrentTime(){return this.player?this.player.currentTime:null}getSecondsLoaded(){if(!this.player)return null;const{buffered:e}=this.player;if(e.length===0)return 0;const t=e.end(e.length-1),r=this.getDuration();return t>r?r:t}getSource(e){const t=this.shouldUseHLS(e),r=this.shouldUseDASH(e),o=this.shouldUseFLV(e);if(!(e instanceof Array||(0,a.isMediaStream)(e)||t||r||o))return C.test(e)?e.replace("www.dropbox.com","dl.dropboxusercontent.com"):e}render(){const{url:e,playing:t,loop:r,controls:o,muted:L,config:n,width:l,height:c}=this.props,x=this.shouldUseAudio(this.props)?"audio":"video",K={width:l==="auto"?l:"100%",height:c==="auto"?c:"100%"};return P.default.createElement(x,{ref:this.ref,src:this.getSource(e),style:K,preload:"auto",autoPlay:t||void 0,controls:o,muted:L,loop:r,...n.attributes},e instanceof Array&&e.map(this.renderSourceElement),n.tracks.map(this.renderTrack))}}return i(g,"displayName","FilePlayer"),i(g,"canPlay",v.canPlay.file),S}var R=J();const $=W(R),Z=z({__proto__:null,default:$},[R]);export{Z as F}; +import{r as q,b as G,d as X,g as W}from"./index-DVa6sjDi.js";function z(u,h){for(var f=0;fp[d]})}}}return Object.freeze(Object.defineProperty(u,Symbol.toStringTag,{value:"Module"}))}var S,A;function J(){if(A)return S;A=1;var u=Object.create,h=Object.defineProperty,f=Object.getOwnPropertyDescriptor,p=Object.getOwnPropertyNames,d=Object.getPrototypeOf,y=Object.prototype.hasOwnProperty,I=(s,e,t)=>e in s?h(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t,D=(s,e)=>{for(var t in e)h(s,t,{get:e[t],enumerable:!0})},b=(s,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of p(e))!y.call(s,o)&&o!==t&&h(s,o,{get:()=>e[o],enumerable:!(r=f(e,o))||r.enumerable});return s},w=(s,e,t)=>(t=s!=null?u(d(s)):{},b(!s||!s.__esModule?h(t,"default",{value:s,enumerable:!0}):t,s)),M=s=>b(h({},"__esModule",{value:!0}),s),i=(s,e,t)=>(I(s,typeof e!="symbol"?e+"":e,t),t),_={};D(_,{default:()=>g}),S=M(_);var P=w(q()),a=G(),v=X();const E=typeof navigator<"u",k=E&&navigator.platform==="MacIntel"&&navigator.maxTouchPoints>1,O=E&&(/iPad|iPhone|iPod/.test(navigator.userAgent)||k)&&!window.MSStream,U=E&&/^((?!chrome|android).)*safari/i.test(navigator.userAgent)&&!window.MSStream,F="https://cdn.jsdelivr.net/npm/hls.js@VERSION/dist/hls.min.js",N="Hls",j="https://cdnjs.cloudflare.com/ajax/libs/dashjs/VERSION/dash.all.min.js",H="dashjs",V="https://cdn.jsdelivr.net/npm/flv.js@VERSION/dist/flv.min.js",T="flvjs",C=/www\.dropbox\.com\/.+/,m=/https:\/\/watch\.cloudflarestream\.com\/([a-z0-9]+)/,B="https://videodelivery.net/{id}/manifest/video.m3u8";class g extends P.Component{constructor(){super(...arguments),i(this,"onReady",(...e)=>this.props.onReady(...e)),i(this,"onPlay",(...e)=>this.props.onPlay(...e)),i(this,"onBuffer",(...e)=>this.props.onBuffer(...e)),i(this,"onBufferEnd",(...e)=>this.props.onBufferEnd(...e)),i(this,"onPause",(...e)=>this.props.onPause(...e)),i(this,"onEnded",(...e)=>this.props.onEnded(...e)),i(this,"onError",(...e)=>this.props.onError(...e)),i(this,"onPlayBackRateChange",e=>this.props.onPlaybackRateChange(e.target.playbackRate)),i(this,"onEnablePIP",(...e)=>this.props.onEnablePIP(...e)),i(this,"onDisablePIP",e=>{const{onDisablePIP:t,playing:r}=this.props;t(e),r&&this.play()}),i(this,"onPresentationModeChange",e=>{if(this.player&&(0,a.supportsWebKitPresentationMode)(this.player)){const{webkitPresentationMode:t}=this.player;t==="picture-in-picture"?this.onEnablePIP(e):t==="inline"&&this.onDisablePIP(e)}}),i(this,"onSeek",e=>{this.props.onSeek(e.target.currentTime)}),i(this,"mute",()=>{this.player.muted=!0}),i(this,"unmute",()=>{this.player.muted=!1}),i(this,"renderSourceElement",(e,t)=>typeof e=="string"?P.default.createElement("source",{key:t,src:e}):P.default.createElement("source",{key:t,...e})),i(this,"renderTrack",(e,t)=>P.default.createElement("track",{key:t,...e})),i(this,"ref",e=>{this.player&&(this.prevPlayer=this.player),this.player=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this),this.addListeners(this.player);const e=this.getSource(this.props.url);e&&(this.player.src=e),(O||this.props.config.forceDisableHls)&&this.player.load()}componentDidUpdate(e){this.shouldUseAudio(this.props)!==this.shouldUseAudio(e)&&(this.removeListeners(this.prevPlayer,e.url),this.addListeners(this.player)),this.props.url!==e.url&&!(0,a.isMediaStream)(this.props.url)&&!(this.props.url instanceof Array)&&(this.player.srcObject=null)}componentWillUnmount(){this.player.removeAttribute("src"),this.removeListeners(this.player),this.hls&&this.hls.destroy()}addListeners(e){const{url:t,playsinline:r}=this.props;e.addEventListener("play",this.onPlay),e.addEventListener("waiting",this.onBuffer),e.addEventListener("playing",this.onBufferEnd),e.addEventListener("pause",this.onPause),e.addEventListener("seeked",this.onSeek),e.addEventListener("ended",this.onEnded),e.addEventListener("error",this.onError),e.addEventListener("ratechange",this.onPlayBackRateChange),e.addEventListener("enterpictureinpicture",this.onEnablePIP),e.addEventListener("leavepictureinpicture",this.onDisablePIP),e.addEventListener("webkitpresentationmodechanged",this.onPresentationModeChange),this.shouldUseHLS(t)||e.addEventListener("canplay",this.onReady),r&&(e.setAttribute("playsinline",""),e.setAttribute("webkit-playsinline",""),e.setAttribute("x5-playsinline",""))}removeListeners(e,t){e.removeEventListener("canplay",this.onReady),e.removeEventListener("play",this.onPlay),e.removeEventListener("waiting",this.onBuffer),e.removeEventListener("playing",this.onBufferEnd),e.removeEventListener("pause",this.onPause),e.removeEventListener("seeked",this.onSeek),e.removeEventListener("ended",this.onEnded),e.removeEventListener("error",this.onError),e.removeEventListener("ratechange",this.onPlayBackRateChange),e.removeEventListener("enterpictureinpicture",this.onEnablePIP),e.removeEventListener("leavepictureinpicture",this.onDisablePIP),e.removeEventListener("webkitpresentationmodechanged",this.onPresentationModeChange),this.shouldUseHLS(t)||e.removeEventListener("canplay",this.onReady)}shouldUseAudio(e){return e.config.forceVideo||e.config.attributes.poster?!1:v.AUDIO_EXTENSIONS.test(e.url)||e.config.forceAudio}shouldUseHLS(e){return U&&this.props.config.forceSafariHLS||this.props.config.forceHLS?!0:O||this.props.config.forceDisableHls?!1:v.HLS_EXTENSIONS.test(e)||m.test(e)}shouldUseDASH(e){return v.DASH_EXTENSIONS.test(e)||this.props.config.forceDASH}shouldUseFLV(e){return v.FLV_EXTENSIONS.test(e)||this.props.config.forceFLV}load(e){const{hlsVersion:t,hlsOptions:r,dashVersion:o,flvVersion:L}=this.props.config;if(this.hls&&this.hls.destroy(),this.dash&&this.dash.reset(),this.shouldUseHLS(e)&&(0,a.getSDK)(F.replace("VERSION",t),N).then(n=>{if(this.hls=new n(r),this.hls.on(n.Events.MANIFEST_PARSED,()=>{this.props.onReady()}),this.hls.on(n.Events.ERROR,(l,c)=>{this.props.onError(l,c,this.hls,n)}),m.test(e)){const l=e.match(m)[1];this.hls.loadSource(B.replace("{id}",l))}else this.hls.loadSource(e);this.hls.attachMedia(this.player),this.props.onLoaded()}),this.shouldUseDASH(e)&&(0,a.getSDK)(j.replace("VERSION",o),H).then(n=>{this.dash=n.MediaPlayer().create(),this.dash.initialize(this.player,e,this.props.playing),this.dash.on("error",this.props.onError),parseInt(o)<3?this.dash.getDebug().setLogToBrowserConsole(!1):this.dash.updateSettings({debug:{logLevel:n.Debug.LOG_LEVEL_NONE}}),this.props.onLoaded()}),this.shouldUseFLV(e)&&(0,a.getSDK)(V.replace("VERSION",L),T).then(n=>{this.flv=n.createPlayer({type:"flv",url:e}),this.flv.attachMediaElement(this.player),this.flv.on(n.Events.ERROR,(l,c)=>{this.props.onError(l,c,this.flv,n)}),this.flv.load(),this.props.onLoaded()}),e instanceof Array)this.player.load();else if((0,a.isMediaStream)(e))try{this.player.srcObject=e}catch{this.player.src=window.URL.createObjectURL(e)}}play(){const e=this.player.play();e&&e.catch(this.props.onError)}pause(){this.player.pause()}stop(){this.player.removeAttribute("src"),this.dash&&this.dash.reset()}seekTo(e,t=!0){this.player.currentTime=e,t||this.pause()}setVolume(e){this.player.volume=e}enablePIP(){this.player.requestPictureInPicture&&document.pictureInPictureElement!==this.player?this.player.requestPictureInPicture():(0,a.supportsWebKitPresentationMode)(this.player)&&this.player.webkitPresentationMode!=="picture-in-picture"&&this.player.webkitSetPresentationMode("picture-in-picture")}disablePIP(){document.exitPictureInPicture&&document.pictureInPictureElement===this.player?document.exitPictureInPicture():(0,a.supportsWebKitPresentationMode)(this.player)&&this.player.webkitPresentationMode!=="inline"&&this.player.webkitSetPresentationMode("inline")}setPlaybackRate(e){try{this.player.playbackRate=e}catch(t){this.props.onError(t)}}getDuration(){if(!this.player)return null;const{duration:e,seekable:t}=this.player;return e===1/0&&t.length>0?t.end(t.length-1):e}getCurrentTime(){return this.player?this.player.currentTime:null}getSecondsLoaded(){if(!this.player)return null;const{buffered:e}=this.player;if(e.length===0)return 0;const t=e.end(e.length-1),r=this.getDuration();return t>r?r:t}getSource(e){const t=this.shouldUseHLS(e),r=this.shouldUseDASH(e),o=this.shouldUseFLV(e);if(!(e instanceof Array||(0,a.isMediaStream)(e)||t||r||o))return C.test(e)?e.replace("www.dropbox.com","dl.dropboxusercontent.com"):e}render(){const{url:e,playing:t,loop:r,controls:o,muted:L,config:n,width:l,height:c}=this.props,x=this.shouldUseAudio(this.props)?"audio":"video",K={width:l==="auto"?l:"100%",height:c==="auto"?c:"100%"};return P.default.createElement(x,{ref:this.ref,src:this.getSource(e),style:K,preload:"auto",autoPlay:t||void 0,controls:o,muted:L,loop:r,...n.attributes},e instanceof Array&&e.map(this.renderSourceElement),n.tracks.map(this.renderTrack))}}return i(g,"displayName","FilePlayer"),i(g,"canPlay",v.canPlay.file),S}var R=J();const $=W(R),Z=z({__proto__:null,default:$},[R]);export{Z as F}; diff --git a/server/src/main/resources/static/assets/Kaltura-CbgdFPgd.js b/server/conductor-agentspan-server/src/main/resources/static/assets/Kaltura-CeCCDjbU.js similarity index 97% rename from server/src/main/resources/static/assets/Kaltura-CbgdFPgd.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/Kaltura-CeCCDjbU.js index 4ab9e1077..cfb4b25f7 100644 --- a/server/src/main/resources/static/assets/Kaltura-CbgdFPgd.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/Kaltura-CeCCDjbU.js @@ -1 +1 @@ -import{r as D,b as M,d as S,g as T}from"./index-DiybVIaQ.js";function E(l,o){for(var u=0;us[n]})}}}return Object.freeze(Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}))}var h,P;function x(){if(P)return h;P=1;var l=Object.create,o=Object.defineProperty,u=Object.getOwnPropertyDescriptor,s=Object.getOwnPropertyNames,n=Object.getPrototypeOf,p=Object.prototype.hasOwnProperty,b=(t,e,r)=>e in t?o(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,v=(t,e)=>{for(var r in e)o(t,r,{get:e[r],enumerable:!0})},y=(t,e,r,c)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of s(e))!p.call(t,i)&&i!==r&&o(t,i,{get:()=>e[i],enumerable:!(c=u(e,i))||c.enumerable});return t},O=(t,e,r)=>(r=t!=null?l(n(t)):{},y(!t||!t.__esModule?o(r,"default",{value:t,enumerable:!0}):r,t)),w=t=>y(o({},"__esModule",{value:!0}),t),a=(t,e,r)=>(b(t,typeof e!="symbol"?e+"":e,r),r),f={};v(f,{default:()=>d}),h=w(f);var _=O(D()),m=M(),K=S();const j="https://cdn.embed.ly/player-0.1.0.min.js",L="playerjs";class d extends _.Component{constructor(){super(...arguments),a(this,"callPlayer",m.callPlayer),a(this,"duration",null),a(this,"currentTime",null),a(this,"secondsLoaded",null),a(this,"mute",()=>{this.callPlayer("mute")}),a(this,"unmute",()=>{this.callPlayer("unmute")}),a(this,"ref",e=>{this.iframe=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){(0,m.getSDK)(j,L).then(r=>{this.iframe&&(this.player=new r.Player(this.iframe),this.player.on("ready",()=>{setTimeout(()=>{this.player.isReady=!0,this.player.setLoop(this.props.loop),this.props.muted&&this.player.mute(),this.addListeners(this.player,this.props),this.props.onReady()},500)}))},this.props.onError)}addListeners(e,r){e.on("play",r.onPlay),e.on("pause",r.onPause),e.on("ended",r.onEnded),e.on("error",r.onError),e.on("timeupdate",({duration:c,seconds:i})=>{this.duration=c,this.currentTime=i})}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){}seekTo(e,r=!0){this.callPlayer("setCurrentTime",e),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e)}setLoop(e){this.callPlayer("setLoop",e)}getDuration(){return this.duration}getCurrentTime(){return this.currentTime}getSecondsLoaded(){return this.secondsLoaded}render(){const e={width:"100%",height:"100%"};return _.default.createElement("iframe",{ref:this.ref,src:this.props.url,frameBorder:"0",scrolling:"no",style:e,allow:"encrypted-media; autoplay; fullscreen;",referrerPolicy:"no-referrer-when-downgrade"})}}return a(d,"displayName","Kaltura"),a(d,"canPlay",K.canPlay.kaltura),h}var g=x();const N=T(g),C=E({__proto__:null,default:N},[g]);export{C as K}; +import{r as D,b as M,d as S,g as T}from"./index-DVa6sjDi.js";function E(l,o){for(var u=0;us[n]})}}}return Object.freeze(Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}))}var h,P;function x(){if(P)return h;P=1;var l=Object.create,o=Object.defineProperty,u=Object.getOwnPropertyDescriptor,s=Object.getOwnPropertyNames,n=Object.getPrototypeOf,p=Object.prototype.hasOwnProperty,b=(t,e,r)=>e in t?o(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,v=(t,e)=>{for(var r in e)o(t,r,{get:e[r],enumerable:!0})},y=(t,e,r,c)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of s(e))!p.call(t,i)&&i!==r&&o(t,i,{get:()=>e[i],enumerable:!(c=u(e,i))||c.enumerable});return t},O=(t,e,r)=>(r=t!=null?l(n(t)):{},y(!t||!t.__esModule?o(r,"default",{value:t,enumerable:!0}):r,t)),w=t=>y(o({},"__esModule",{value:!0}),t),a=(t,e,r)=>(b(t,typeof e!="symbol"?e+"":e,r),r),f={};v(f,{default:()=>d}),h=w(f);var _=O(D()),m=M(),K=S();const j="https://cdn.embed.ly/player-0.1.0.min.js",L="playerjs";class d extends _.Component{constructor(){super(...arguments),a(this,"callPlayer",m.callPlayer),a(this,"duration",null),a(this,"currentTime",null),a(this,"secondsLoaded",null),a(this,"mute",()=>{this.callPlayer("mute")}),a(this,"unmute",()=>{this.callPlayer("unmute")}),a(this,"ref",e=>{this.iframe=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){(0,m.getSDK)(j,L).then(r=>{this.iframe&&(this.player=new r.Player(this.iframe),this.player.on("ready",()=>{setTimeout(()=>{this.player.isReady=!0,this.player.setLoop(this.props.loop),this.props.muted&&this.player.mute(),this.addListeners(this.player,this.props),this.props.onReady()},500)}))},this.props.onError)}addListeners(e,r){e.on("play",r.onPlay),e.on("pause",r.onPause),e.on("ended",r.onEnded),e.on("error",r.onError),e.on("timeupdate",({duration:c,seconds:i})=>{this.duration=c,this.currentTime=i})}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){}seekTo(e,r=!0){this.callPlayer("setCurrentTime",e),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e)}setLoop(e){this.callPlayer("setLoop",e)}getDuration(){return this.duration}getCurrentTime(){return this.currentTime}getSecondsLoaded(){return this.secondsLoaded}render(){const e={width:"100%",height:"100%"};return _.default.createElement("iframe",{ref:this.ref,src:this.props.url,frameBorder:"0",scrolling:"no",style:e,allow:"encrypted-media; autoplay; fullscreen;",referrerPolicy:"no-referrer-when-downgrade"})}}return a(d,"displayName","Kaltura"),a(d,"canPlay",K.canPlay.kaltura),h}var g=x();const N=T(g),C=E({__proto__:null,default:N},[g]);export{C as K}; diff --git a/server/src/main/resources/static/assets/Mixcloud-CnLXhrD1.js b/server/conductor-agentspan-server/src/main/resources/static/assets/Mixcloud-adpnMd5P.js similarity index 97% rename from server/src/main/resources/static/assets/Mixcloud-CnLXhrD1.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/Mixcloud-adpnMd5P.js index bff017061..6b9698988 100644 --- a/server/src/main/resources/static/assets/Mixcloud-CnLXhrD1.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/Mixcloud-adpnMd5P.js @@ -1 +1 @@ -import{r as S,b as q,d as E,g as L}from"./index-DiybVIaQ.js";function T(u,a){for(var p=0;pi[n]})}}}return Object.freeze(Object.defineProperty(u,Symbol.toStringTag,{value:"Module"}))}var y,v;function C(){if(v)return y;v=1;var u=Object.create,a=Object.defineProperty,p=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,n=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,O=(r,e,t)=>e in r?a(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,b=(r,e)=>{for(var t in e)a(r,t,{get:e[t],enumerable:!0})},_=(r,e,t,l)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of i(e))!c.call(r,s)&&s!==t&&a(r,s,{get:()=>e[s],enumerable:!(l=p(e,s))||l.enumerable});return r},M=(r,e,t)=>(t=r!=null?u(n(r)):{},_(!r||!r.__esModule?a(t,"default",{value:r,enumerable:!0}):t,r)),x=r=>_(a({},"__esModule",{value:!0}),r),o=(r,e,t)=>(O(r,typeof e!="symbol"?e+"":e,t),t),f={};b(f,{default:()=>d}),y=x(f);var m=M(S()),h=q(),g=E();const w="https://widget.mixcloud.com/media/js/widgetApi.js",j="Mixcloud";class d extends m.Component{constructor(){super(...arguments),o(this,"callPlayer",h.callPlayer),o(this,"duration",null),o(this,"currentTime",null),o(this,"secondsLoaded",null),o(this,"mute",()=>{}),o(this,"unmute",()=>{}),o(this,"ref",e=>{this.iframe=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){(0,h.getSDK)(w,j).then(t=>{this.player=t.PlayerWidget(this.iframe),this.player.ready.then(()=>{this.player.events.play.on(this.props.onPlay),this.player.events.pause.on(this.props.onPause),this.player.events.ended.on(this.props.onEnded),this.player.events.error.on(this.props.error),this.player.events.progress.on((l,s)=>{this.currentTime=l,this.duration=s}),this.props.onReady()})},this.props.onError)}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){}seekTo(e,t=!0){this.callPlayer("seek",e),t||this.pause()}setVolume(e){}getDuration(){return this.duration}getCurrentTime(){return this.currentTime}getSecondsLoaded(){return null}render(){const{url:e,config:t}=this.props,l=e.match(g.MATCH_URL_MIXCLOUD)[1],s={width:"100%",height:"100%"},D=(0,h.queryString)({...t.options,feed:`/${l}/`});return m.default.createElement("iframe",{key:l,ref:this.ref,style:s,src:`https://player-widget.mixcloud.com/widget/iframe/?${D}`,frameBorder:"0",allow:"autoplay"})}}return o(d,"displayName","Mixcloud"),o(d,"canPlay",g.canPlay.mixcloud),o(d,"loopOnEnded",!0),y}var P=C();const N=L(P),R=T({__proto__:null,default:N},[P]);export{R as M}; +import{r as S,b as q,d as E,g as L}from"./index-DVa6sjDi.js";function T(u,a){for(var p=0;pi[n]})}}}return Object.freeze(Object.defineProperty(u,Symbol.toStringTag,{value:"Module"}))}var y,v;function C(){if(v)return y;v=1;var u=Object.create,a=Object.defineProperty,p=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,n=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,O=(r,e,t)=>e in r?a(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,b=(r,e)=>{for(var t in e)a(r,t,{get:e[t],enumerable:!0})},_=(r,e,t,l)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of i(e))!c.call(r,s)&&s!==t&&a(r,s,{get:()=>e[s],enumerable:!(l=p(e,s))||l.enumerable});return r},M=(r,e,t)=>(t=r!=null?u(n(r)):{},_(!r||!r.__esModule?a(t,"default",{value:r,enumerable:!0}):t,r)),x=r=>_(a({},"__esModule",{value:!0}),r),o=(r,e,t)=>(O(r,typeof e!="symbol"?e+"":e,t),t),f={};b(f,{default:()=>d}),y=x(f);var m=M(S()),h=q(),g=E();const w="https://widget.mixcloud.com/media/js/widgetApi.js",j="Mixcloud";class d extends m.Component{constructor(){super(...arguments),o(this,"callPlayer",h.callPlayer),o(this,"duration",null),o(this,"currentTime",null),o(this,"secondsLoaded",null),o(this,"mute",()=>{}),o(this,"unmute",()=>{}),o(this,"ref",e=>{this.iframe=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){(0,h.getSDK)(w,j).then(t=>{this.player=t.PlayerWidget(this.iframe),this.player.ready.then(()=>{this.player.events.play.on(this.props.onPlay),this.player.events.pause.on(this.props.onPause),this.player.events.ended.on(this.props.onEnded),this.player.events.error.on(this.props.error),this.player.events.progress.on((l,s)=>{this.currentTime=l,this.duration=s}),this.props.onReady()})},this.props.onError)}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){}seekTo(e,t=!0){this.callPlayer("seek",e),t||this.pause()}setVolume(e){}getDuration(){return this.duration}getCurrentTime(){return this.currentTime}getSecondsLoaded(){return null}render(){const{url:e,config:t}=this.props,l=e.match(g.MATCH_URL_MIXCLOUD)[1],s={width:"100%",height:"100%"},D=(0,h.queryString)({...t.options,feed:`/${l}/`});return m.default.createElement("iframe",{key:l,ref:this.ref,style:s,src:`https://player-widget.mixcloud.com/widget/iframe/?${D}`,frameBorder:"0",allow:"autoplay"})}}return o(d,"displayName","Mixcloud"),o(d,"canPlay",g.canPlay.mixcloud),o(d,"loopOnEnded",!0),y}var P=C();const N=L(P),R=T({__proto__:null,default:N},[P]);export{R as M}; diff --git a/server/src/main/resources/static/assets/Mux-Bx64MX_j.js b/server/conductor-agentspan-server/src/main/resources/static/assets/Mux-8MezvdiC.js similarity index 98% rename from server/src/main/resources/static/assets/Mux-Bx64MX_j.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/Mux-8MezvdiC.js index 041843d94..8a5831e7e 100644 --- a/server/src/main/resources/static/assets/Mux-Bx64MX_j.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/Mux-8MezvdiC.js @@ -1 +1 @@ -import{r as w,_ as D,d as j,g as C}from"./index-DiybVIaQ.js";function S(p,o){for(var l=0;la[u]})}}}return Object.freeze(Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}))}var y,L;function B(){if(L)return y;L=1;var p=Object.create,o=Object.defineProperty,l=Object.getOwnPropertyDescriptor,a=Object.getOwnPropertyNames,u=Object.getPrototypeOf,h=Object.prototype.hasOwnProperty,k=(r,e,t)=>e in r?o(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,x=(r,e)=>{for(var t in e)o(r,t,{get:e[t],enumerable:!0})},P=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of a(e))!h.call(r,s)&&s!==t&&o(r,s,{get:()=>e[s],enumerable:!(i=l(e,s))||i.enumerable});return r},M=(r,e,t)=>(t=r!=null?p(u(r)):{},P(!r||!r.__esModule?o(t,"default",{value:r,enumerable:!0}):t,r)),O=r=>P(o({},"__esModule",{value:!0}),r),n=(r,e,t)=>(k(r,typeof e!="symbol"?e+"":e,t),t),m={};x(m,{default:()=>v}),y=O(m);var E=M(w()),d=j();const R="https://cdn.jsdelivr.net/npm/@mux/mux-player@VERSION/dist/mux-player.mjs";class v extends E.Component{constructor(){super(...arguments),n(this,"onReady",(...e)=>this.props.onReady(...e)),n(this,"onPlay",(...e)=>this.props.onPlay(...e)),n(this,"onBuffer",(...e)=>this.props.onBuffer(...e)),n(this,"onBufferEnd",(...e)=>this.props.onBufferEnd(...e)),n(this,"onPause",(...e)=>this.props.onPause(...e)),n(this,"onEnded",(...e)=>this.props.onEnded(...e)),n(this,"onError",(...e)=>this.props.onError(...e)),n(this,"onPlayBackRateChange",e=>this.props.onPlaybackRateChange(e.target.playbackRate)),n(this,"onEnablePIP",(...e)=>this.props.onEnablePIP(...e)),n(this,"onSeek",e=>{this.props.onSeek(e.target.currentTime)}),n(this,"onDurationChange",()=>{const e=this.getDuration();this.props.onDuration(e)}),n(this,"mute",()=>{this.player.muted=!0}),n(this,"unmute",()=>{this.player.muted=!1}),n(this,"ref",e=>{this.player=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this),this.addListeners(this.player);const e=this.getPlaybackId(this.props.url);e&&(this.player.playbackId=e)}componentWillUnmount(){this.player.playbackId=null,this.removeListeners(this.player)}addListeners(e){const{playsinline:t}=this.props;e.addEventListener("play",this.onPlay),e.addEventListener("waiting",this.onBuffer),e.addEventListener("playing",this.onBufferEnd),e.addEventListener("pause",this.onPause),e.addEventListener("seeked",this.onSeek),e.addEventListener("ended",this.onEnded),e.addEventListener("error",this.onError),e.addEventListener("ratechange",this.onPlayBackRateChange),e.addEventListener("enterpictureinpicture",this.onEnablePIP),e.addEventListener("leavepictureinpicture",this.onDisablePIP),e.addEventListener("webkitpresentationmodechanged",this.onPresentationModeChange),e.addEventListener("canplay",this.onReady),t&&e.setAttribute("playsinline","")}removeListeners(e){e.removeEventListener("canplay",this.onReady),e.removeEventListener("play",this.onPlay),e.removeEventListener("waiting",this.onBuffer),e.removeEventListener("playing",this.onBufferEnd),e.removeEventListener("pause",this.onPause),e.removeEventListener("seeked",this.onSeek),e.removeEventListener("ended",this.onEnded),e.removeEventListener("error",this.onError),e.removeEventListener("ratechange",this.onPlayBackRateChange),e.removeEventListener("enterpictureinpicture",this.onEnablePIP),e.removeEventListener("leavepictureinpicture",this.onDisablePIP),e.removeEventListener("canplay",this.onReady)}async load(e){var t;const{onError:i,config:s}=this.props;if(!((t=globalThis.customElements)!=null&&t.get("mux-player")))try{const c=R.replace("VERSION",s.version);await D(()=>import(`${c}`),[]),this.props.onLoaded()}catch(c){i(c)}const[,f]=e.match(d.MATCH_URL_MUX);this.player.playbackId=f}play(){const e=this.player.play();e&&e.catch(this.props.onError)}pause(){this.player.pause()}stop(){this.player.playbackId=null}seekTo(e,t=!0){this.player.currentTime=e,t||this.pause()}setVolume(e){this.player.volume=e}enablePIP(){this.player.requestPictureInPicture&&document.pictureInPictureElement!==this.player&&this.player.requestPictureInPicture()}disablePIP(){document.exitPictureInPicture&&document.pictureInPictureElement===this.player&&document.exitPictureInPicture()}setPlaybackRate(e){try{this.player.playbackRate=e}catch(t){this.props.onError(t)}}getDuration(){if(!this.player)return null;const{duration:e,seekable:t}=this.player;return e===1/0&&t.length>0?t.end(t.length-1):e}getCurrentTime(){return this.player?this.player.currentTime:null}getSecondsLoaded(){if(!this.player)return null;const{buffered:e}=this.player;if(e.length===0)return 0;const t=e.end(e.length-1),i=this.getDuration();return t>i?i:t}getPlaybackId(e){const[,t]=e.match(d.MATCH_URL_MUX);return t}render(){const{url:e,playing:t,loop:i,controls:s,muted:f,config:c,width:g,height:_}=this.props,b={width:g==="auto"?g:"100%",height:_==="auto"?_:"100%"};return s===!1&&(b["--controls"]="none"),E.default.createElement("mux-player",{ref:this.ref,"playback-id":this.getPlaybackId(e),style:b,preload:"auto",autoPlay:t||void 0,muted:f?"":void 0,loop:i?"":void 0,...c.attributes})}}return n(v,"displayName","Mux"),n(v,"canPlay",d.canPlay.mux),y}var I=B();const T=C(I),U=S({__proto__:null,default:T},[I]);export{U as M}; +import{r as w,_ as D,d as j,g as C}from"./index-DVa6sjDi.js";function S(p,o){for(var l=0;la[u]})}}}return Object.freeze(Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}))}var y,L;function B(){if(L)return y;L=1;var p=Object.create,o=Object.defineProperty,l=Object.getOwnPropertyDescriptor,a=Object.getOwnPropertyNames,u=Object.getPrototypeOf,h=Object.prototype.hasOwnProperty,k=(r,e,t)=>e in r?o(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,x=(r,e)=>{for(var t in e)o(r,t,{get:e[t],enumerable:!0})},P=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of a(e))!h.call(r,s)&&s!==t&&o(r,s,{get:()=>e[s],enumerable:!(i=l(e,s))||i.enumerable});return r},M=(r,e,t)=>(t=r!=null?p(u(r)):{},P(!r||!r.__esModule?o(t,"default",{value:r,enumerable:!0}):t,r)),O=r=>P(o({},"__esModule",{value:!0}),r),n=(r,e,t)=>(k(r,typeof e!="symbol"?e+"":e,t),t),m={};x(m,{default:()=>v}),y=O(m);var E=M(w()),d=j();const R="https://cdn.jsdelivr.net/npm/@mux/mux-player@VERSION/dist/mux-player.mjs";class v extends E.Component{constructor(){super(...arguments),n(this,"onReady",(...e)=>this.props.onReady(...e)),n(this,"onPlay",(...e)=>this.props.onPlay(...e)),n(this,"onBuffer",(...e)=>this.props.onBuffer(...e)),n(this,"onBufferEnd",(...e)=>this.props.onBufferEnd(...e)),n(this,"onPause",(...e)=>this.props.onPause(...e)),n(this,"onEnded",(...e)=>this.props.onEnded(...e)),n(this,"onError",(...e)=>this.props.onError(...e)),n(this,"onPlayBackRateChange",e=>this.props.onPlaybackRateChange(e.target.playbackRate)),n(this,"onEnablePIP",(...e)=>this.props.onEnablePIP(...e)),n(this,"onSeek",e=>{this.props.onSeek(e.target.currentTime)}),n(this,"onDurationChange",()=>{const e=this.getDuration();this.props.onDuration(e)}),n(this,"mute",()=>{this.player.muted=!0}),n(this,"unmute",()=>{this.player.muted=!1}),n(this,"ref",e=>{this.player=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this),this.addListeners(this.player);const e=this.getPlaybackId(this.props.url);e&&(this.player.playbackId=e)}componentWillUnmount(){this.player.playbackId=null,this.removeListeners(this.player)}addListeners(e){const{playsinline:t}=this.props;e.addEventListener("play",this.onPlay),e.addEventListener("waiting",this.onBuffer),e.addEventListener("playing",this.onBufferEnd),e.addEventListener("pause",this.onPause),e.addEventListener("seeked",this.onSeek),e.addEventListener("ended",this.onEnded),e.addEventListener("error",this.onError),e.addEventListener("ratechange",this.onPlayBackRateChange),e.addEventListener("enterpictureinpicture",this.onEnablePIP),e.addEventListener("leavepictureinpicture",this.onDisablePIP),e.addEventListener("webkitpresentationmodechanged",this.onPresentationModeChange),e.addEventListener("canplay",this.onReady),t&&e.setAttribute("playsinline","")}removeListeners(e){e.removeEventListener("canplay",this.onReady),e.removeEventListener("play",this.onPlay),e.removeEventListener("waiting",this.onBuffer),e.removeEventListener("playing",this.onBufferEnd),e.removeEventListener("pause",this.onPause),e.removeEventListener("seeked",this.onSeek),e.removeEventListener("ended",this.onEnded),e.removeEventListener("error",this.onError),e.removeEventListener("ratechange",this.onPlayBackRateChange),e.removeEventListener("enterpictureinpicture",this.onEnablePIP),e.removeEventListener("leavepictureinpicture",this.onDisablePIP),e.removeEventListener("canplay",this.onReady)}async load(e){var t;const{onError:i,config:s}=this.props;if(!((t=globalThis.customElements)!=null&&t.get("mux-player")))try{const c=R.replace("VERSION",s.version);await D(()=>import(`${c}`),[]),this.props.onLoaded()}catch(c){i(c)}const[,f]=e.match(d.MATCH_URL_MUX);this.player.playbackId=f}play(){const e=this.player.play();e&&e.catch(this.props.onError)}pause(){this.player.pause()}stop(){this.player.playbackId=null}seekTo(e,t=!0){this.player.currentTime=e,t||this.pause()}setVolume(e){this.player.volume=e}enablePIP(){this.player.requestPictureInPicture&&document.pictureInPictureElement!==this.player&&this.player.requestPictureInPicture()}disablePIP(){document.exitPictureInPicture&&document.pictureInPictureElement===this.player&&document.exitPictureInPicture()}setPlaybackRate(e){try{this.player.playbackRate=e}catch(t){this.props.onError(t)}}getDuration(){if(!this.player)return null;const{duration:e,seekable:t}=this.player;return e===1/0&&t.length>0?t.end(t.length-1):e}getCurrentTime(){return this.player?this.player.currentTime:null}getSecondsLoaded(){if(!this.player)return null;const{buffered:e}=this.player;if(e.length===0)return 0;const t=e.end(e.length-1),i=this.getDuration();return t>i?i:t}getPlaybackId(e){const[,t]=e.match(d.MATCH_URL_MUX);return t}render(){const{url:e,playing:t,loop:i,controls:s,muted:f,config:c,width:g,height:_}=this.props,b={width:g==="auto"?g:"100%",height:_==="auto"?_:"100%"};return s===!1&&(b["--controls"]="none"),E.default.createElement("mux-player",{ref:this.ref,"playback-id":this.getPlaybackId(e),style:b,preload:"auto",autoPlay:t||void 0,muted:f?"":void 0,loop:i?"":void 0,...c.attributes})}}return n(v,"displayName","Mux"),n(v,"canPlay",d.canPlay.mux),y}var I=B();const T=C(I),U=S({__proto__:null,default:T},[I]);export{U as M}; diff --git a/server/src/main/resources/static/assets/Preview-CaZuZa6m.js b/server/conductor-agentspan-server/src/main/resources/static/assets/Preview-BMOGze4w.js similarity index 97% rename from server/src/main/resources/static/assets/Preview-CaZuZa6m.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/Preview-BMOGze4w.js index 5bba81618..e354ac3a9 100644 --- a/server/src/main/resources/static/assets/Preview-CaZuZa6m.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/Preview-BMOGze4w.js @@ -1 +1 @@ -import{r as k,g as D}from"./index-DiybVIaQ.js";function M(c,n){for(var p=0;po[s]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v,O;function q(){if(O)return v;O=1;var c=Object.create,n=Object.defineProperty,p=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,u=Object.prototype.hasOwnProperty,I=(r,e,t)=>e in r?n(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,j=(r,e)=>{for(var t in e)n(r,t,{get:e[t],enumerable:!0})},b=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of o(e))!u.call(r,a)&&a!==t&&n(r,a,{get:()=>e[a],enumerable:!(i=p(e,a))||i.enumerable});return r},E=(r,e,t)=>(t=r!=null?c(s(r)):{},b(!r||!r.__esModule?n(t,"default",{value:r,enumerable:!0}):t,r)),S=r=>b(n({},"__esModule",{value:!0}),r),f=(r,e,t)=>(I(r,typeof e!="symbol"?e+"":e,t),t),y={};j(y,{default:()=>C}),v=S(y);var l=E(k());const h="64px",_={};class C extends l.Component{constructor(){super(...arguments),f(this,"mounted",!1),f(this,"state",{image:null}),f(this,"handleKeyPress",e=>{(e.key==="Enter"||e.key===" ")&&this.props.onClick()})}componentDidMount(){this.mounted=!0,this.fetchImage(this.props)}componentDidUpdate(e){const{url:t,light:i}=this.props;(e.url!==t||e.light!==i)&&this.fetchImage(this.props)}componentWillUnmount(){this.mounted=!1}fetchImage({url:e,light:t,oEmbedUrl:i}){if(!l.default.isValidElement(t)){if(typeof t=="string"){this.setState({image:t});return}if(_[e]){this.setState({image:_[e]});return}return this.setState({image:null}),window.fetch(i.replace("{url}",e)).then(a=>a.json()).then(a=>{if(a.thumbnail_url&&this.mounted){const d=a.thumbnail_url.replace("height=100","height=480").replace("-d_295x166","-d_640");this.setState({image:d}),_[e]=d}})}}render(){const{light:e,onClick:t,playIcon:i,previewTabIndex:a,previewAriaLabel:d}=this.props,{image:w}=this.state,g=l.default.isValidElement(e),P={display:"flex",alignItems:"center",justifyContent:"center"},m={preview:{width:"100%",height:"100%",backgroundImage:w&&!g?`url(${w})`:void 0,backgroundSize:"cover",backgroundPosition:"center",cursor:"pointer",...P},shadow:{background:"radial-gradient(rgb(0, 0, 0, 0.3), rgba(0, 0, 0, 0) 60%)",borderRadius:h,width:h,height:h,position:g?"absolute":void 0,...P},playIcon:{borderStyle:"solid",borderWidth:"16px 0 16px 26px",borderColor:"transparent transparent transparent white",marginLeft:"7px"}},N=l.default.createElement("div",{style:m.shadow,className:"react-player__shadow"},l.default.createElement("div",{style:m.playIcon,className:"react-player__play-icon"}));return l.default.createElement("div",{style:m.preview,className:"react-player__preview",onClick:t,tabIndex:a,onKeyPress:this.handleKeyPress,...d?{"aria-label":d}:{}},g?e:null,i||N)}}return v}var x=q();const A=D(x),R=M({__proto__:null,default:A},[x]);export{R as P}; +import{r as k,g as D}from"./index-DVa6sjDi.js";function M(c,n){for(var p=0;po[s]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v,O;function q(){if(O)return v;O=1;var c=Object.create,n=Object.defineProperty,p=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,u=Object.prototype.hasOwnProperty,I=(r,e,t)=>e in r?n(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,j=(r,e)=>{for(var t in e)n(r,t,{get:e[t],enumerable:!0})},b=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of o(e))!u.call(r,a)&&a!==t&&n(r,a,{get:()=>e[a],enumerable:!(i=p(e,a))||i.enumerable});return r},E=(r,e,t)=>(t=r!=null?c(s(r)):{},b(!r||!r.__esModule?n(t,"default",{value:r,enumerable:!0}):t,r)),S=r=>b(n({},"__esModule",{value:!0}),r),f=(r,e,t)=>(I(r,typeof e!="symbol"?e+"":e,t),t),y={};j(y,{default:()=>C}),v=S(y);var l=E(k());const h="64px",_={};class C extends l.Component{constructor(){super(...arguments),f(this,"mounted",!1),f(this,"state",{image:null}),f(this,"handleKeyPress",e=>{(e.key==="Enter"||e.key===" ")&&this.props.onClick()})}componentDidMount(){this.mounted=!0,this.fetchImage(this.props)}componentDidUpdate(e){const{url:t,light:i}=this.props;(e.url!==t||e.light!==i)&&this.fetchImage(this.props)}componentWillUnmount(){this.mounted=!1}fetchImage({url:e,light:t,oEmbedUrl:i}){if(!l.default.isValidElement(t)){if(typeof t=="string"){this.setState({image:t});return}if(_[e]){this.setState({image:_[e]});return}return this.setState({image:null}),window.fetch(i.replace("{url}",e)).then(a=>a.json()).then(a=>{if(a.thumbnail_url&&this.mounted){const d=a.thumbnail_url.replace("height=100","height=480").replace("-d_295x166","-d_640");this.setState({image:d}),_[e]=d}})}}render(){const{light:e,onClick:t,playIcon:i,previewTabIndex:a,previewAriaLabel:d}=this.props,{image:w}=this.state,g=l.default.isValidElement(e),P={display:"flex",alignItems:"center",justifyContent:"center"},m={preview:{width:"100%",height:"100%",backgroundImage:w&&!g?`url(${w})`:void 0,backgroundSize:"cover",backgroundPosition:"center",cursor:"pointer",...P},shadow:{background:"radial-gradient(rgb(0, 0, 0, 0.3), rgba(0, 0, 0, 0) 60%)",borderRadius:h,width:h,height:h,position:g?"absolute":void 0,...P},playIcon:{borderStyle:"solid",borderWidth:"16px 0 16px 26px",borderColor:"transparent transparent transparent white",marginLeft:"7px"}},N=l.default.createElement("div",{style:m.shadow,className:"react-player__shadow"},l.default.createElement("div",{style:m.playIcon,className:"react-player__play-icon"}));return l.default.createElement("div",{style:m.preview,className:"react-player__preview",onClick:t,tabIndex:a,onKeyPress:this.handleKeyPress,...d?{"aria-label":d}:{}},g?e:null,i||N)}}return v}var x=q();const A=D(x),R=M({__proto__:null,default:A},[x]);export{R as P}; diff --git a/server/src/main/resources/static/assets/SoundCloud-C1lS-_i6.js b/server/conductor-agentspan-server/src/main/resources/static/assets/SoundCloud-_btX2qkW.js similarity index 97% rename from server/src/main/resources/static/assets/SoundCloud-C1lS-_i6.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/SoundCloud-_btX2qkW.js index b1475ce0e..555462b2b 100644 --- a/server/src/main/resources/static/assets/SoundCloud-C1lS-_i6.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/SoundCloud-_btX2qkW.js @@ -1 +1 @@ -import{r as T,b as N,d as x,g as A}from"./index-DiybVIaQ.js";function q(l,s){for(var p=0;pn[i]})}}}return Object.freeze(Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}))}var f,g;function U(){if(g)return f;g=1;var l=Object.create,s=Object.defineProperty,p=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,i=Object.getPrototypeOf,d=Object.prototype.hasOwnProperty,v=(t,e,r)=>e in t?s(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,O=(t,e)=>{for(var r in e)s(t,r,{get:e[r],enumerable:!0})},y=(t,e,r,c)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of n(e))!d.call(t,a)&&a!==r&&s(t,a,{get:()=>e[a],enumerable:!(c=p(e,a))||c.enumerable});return t},S=(t,e,r)=>(r=t!=null?l(i(t)):{},y(!t||!t.__esModule?s(r,"default",{value:t,enumerable:!0}):r,t)),C=t=>y(s({},"__esModule",{value:!0}),t),o=(t,e,r)=>(v(t,typeof e!="symbol"?e+"":e,r),r),_={};O(_,{default:()=>h}),f=C(_);var m=S(T()),P=N(),w=x();const j="https://w.soundcloud.com/player/api.js",E="SC";class h extends m.Component{constructor(){super(...arguments),o(this,"callPlayer",P.callPlayer),o(this,"duration",null),o(this,"currentTime",null),o(this,"fractionLoaded",null),o(this,"mute",()=>{this.setVolume(0)}),o(this,"unmute",()=>{this.props.volume!==null&&this.setVolume(this.props.volume)}),o(this,"ref",e=>{this.iframe=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e,r){(0,P.getSDK)(j,E).then(c=>{if(!this.iframe)return;const{PLAY:a,PLAY_PROGRESS:D,PAUSE:R,FINISH:L,ERROR:M}=c.Widget.Events;r||(this.player=c.Widget(this.iframe),this.player.bind(a,this.props.onPlay),this.player.bind(R,()=>{this.duration-this.currentTime<.05||this.props.onPause()}),this.player.bind(D,u=>{this.currentTime=u.currentPosition/1e3,this.fractionLoaded=u.loadedProgress}),this.player.bind(L,()=>this.props.onEnded()),this.player.bind(M,u=>this.props.onError(u))),this.player.load(e,{...this.props.config.options,callback:()=>{this.player.getDuration(u=>{this.duration=u/1e3,this.props.onReady()})}})})}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){}seekTo(e,r=!0){this.callPlayer("seekTo",e*1e3),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e*100)}getDuration(){return this.duration}getCurrentTime(){return this.currentTime}getSecondsLoaded(){return this.fractionLoaded*this.duration}render(){const{display:e}=this.props,r={width:"100%",height:"100%",display:e};return m.default.createElement("iframe",{ref:this.ref,src:`https://w.soundcloud.com/player/?url=${encodeURIComponent(this.props.url)}`,style:r,frameBorder:0,allow:"autoplay"})}}return o(h,"displayName","SoundCloud"),o(h,"canPlay",w.canPlay.soundcloud),o(h,"loopOnEnded",!0),f}var b=U();const V=A(b),I=q({__proto__:null,default:V},[b]);export{I as S}; +import{r as T,b as N,d as x,g as A}from"./index-DVa6sjDi.js";function q(l,s){for(var p=0;pn[i]})}}}return Object.freeze(Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}))}var f,g;function U(){if(g)return f;g=1;var l=Object.create,s=Object.defineProperty,p=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,i=Object.getPrototypeOf,d=Object.prototype.hasOwnProperty,v=(t,e,r)=>e in t?s(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,O=(t,e)=>{for(var r in e)s(t,r,{get:e[r],enumerable:!0})},y=(t,e,r,c)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of n(e))!d.call(t,a)&&a!==r&&s(t,a,{get:()=>e[a],enumerable:!(c=p(e,a))||c.enumerable});return t},S=(t,e,r)=>(r=t!=null?l(i(t)):{},y(!t||!t.__esModule?s(r,"default",{value:t,enumerable:!0}):r,t)),C=t=>y(s({},"__esModule",{value:!0}),t),o=(t,e,r)=>(v(t,typeof e!="symbol"?e+"":e,r),r),_={};O(_,{default:()=>h}),f=C(_);var m=S(T()),P=N(),w=x();const j="https://w.soundcloud.com/player/api.js",E="SC";class h extends m.Component{constructor(){super(...arguments),o(this,"callPlayer",P.callPlayer),o(this,"duration",null),o(this,"currentTime",null),o(this,"fractionLoaded",null),o(this,"mute",()=>{this.setVolume(0)}),o(this,"unmute",()=>{this.props.volume!==null&&this.setVolume(this.props.volume)}),o(this,"ref",e=>{this.iframe=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e,r){(0,P.getSDK)(j,E).then(c=>{if(!this.iframe)return;const{PLAY:a,PLAY_PROGRESS:D,PAUSE:R,FINISH:L,ERROR:M}=c.Widget.Events;r||(this.player=c.Widget(this.iframe),this.player.bind(a,this.props.onPlay),this.player.bind(R,()=>{this.duration-this.currentTime<.05||this.props.onPause()}),this.player.bind(D,u=>{this.currentTime=u.currentPosition/1e3,this.fractionLoaded=u.loadedProgress}),this.player.bind(L,()=>this.props.onEnded()),this.player.bind(M,u=>this.props.onError(u))),this.player.load(e,{...this.props.config.options,callback:()=>{this.player.getDuration(u=>{this.duration=u/1e3,this.props.onReady()})}})})}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){}seekTo(e,r=!0){this.callPlayer("seekTo",e*1e3),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e*100)}getDuration(){return this.duration}getCurrentTime(){return this.currentTime}getSecondsLoaded(){return this.fractionLoaded*this.duration}render(){const{display:e}=this.props,r={width:"100%",height:"100%",display:e};return m.default.createElement("iframe",{ref:this.ref,src:`https://w.soundcloud.com/player/?url=${encodeURIComponent(this.props.url)}`,style:r,frameBorder:0,allow:"autoplay"})}}return o(h,"displayName","SoundCloud"),o(h,"canPlay",w.canPlay.soundcloud),o(h,"loopOnEnded",!0),f}var b=U();const V=A(b),I=q({__proto__:null,default:V},[b]);export{I as S}; diff --git a/server/src/main/resources/static/assets/Streamable-Ctz4Qtsg.js b/server/conductor-agentspan-server/src/main/resources/static/assets/Streamable-B4sBlscb.js similarity index 97% rename from server/src/main/resources/static/assets/Streamable-Ctz4Qtsg.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/Streamable-B4sBlscb.js index 7fc205d42..e4a44f209 100644 --- a/server/src/main/resources/static/assets/Streamable-Ctz4Qtsg.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/Streamable-B4sBlscb.js @@ -1 +1 @@ -import{r as M,b as D,d as E,g as T}from"./index-DiybVIaQ.js";function x(p,s){for(var u=0;uo[i]})}}}return Object.freeze(Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}))}var d,P;function A(){if(P)return d;P=1;var p=Object.create,s=Object.defineProperty,u=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,i=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,v=(r,e,t)=>e in r?s(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,O=(r,e)=>{for(var t in e)s(r,t,{get:e[t],enumerable:!0})},y=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let l of o(e))!c.call(r,l)&&l!==t&&s(r,l,{get:()=>e[l],enumerable:!(n=u(e,l))||n.enumerable});return r},S=(r,e,t)=>(t=r!=null?p(i(r)):{},y(!r||!r.__esModule?s(t,"default",{value:r,enumerable:!0}):t,r)),j=r=>y(s({},"__esModule",{value:!0}),r),a=(r,e,t)=>(v(r,typeof e!="symbol"?e+"":e,t),t),m={};O(m,{default:()=>h}),d=j(m);var f=S(M()),_=D(),b=E();const L="https://cdn.embed.ly/player-0.1.0.min.js",w="playerjs";class h extends f.Component{constructor(){super(...arguments),a(this,"callPlayer",_.callPlayer),a(this,"duration",null),a(this,"currentTime",null),a(this,"secondsLoaded",null),a(this,"mute",()=>{this.callPlayer("mute")}),a(this,"unmute",()=>{this.callPlayer("unmute")}),a(this,"ref",e=>{this.iframe=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){(0,_.getSDK)(L,w).then(t=>{this.iframe&&(this.player=new t.Player(this.iframe),this.player.setLoop(this.props.loop),this.player.on("ready",this.props.onReady),this.player.on("play",this.props.onPlay),this.player.on("pause",this.props.onPause),this.player.on("seeked",this.props.onSeek),this.player.on("ended",this.props.onEnded),this.player.on("error",this.props.onError),this.player.on("timeupdate",({duration:n,seconds:l})=>{this.duration=n,this.currentTime=l}),this.player.on("buffered",({percent:n})=>{this.duration&&(this.secondsLoaded=this.duration*n)}),this.props.muted&&this.player.mute())},this.props.onError)}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){}seekTo(e,t=!0){this.callPlayer("setCurrentTime",e),t||this.pause()}setVolume(e){this.callPlayer("setVolume",e*100)}setLoop(e){this.callPlayer("setLoop",e)}getDuration(){return this.duration}getCurrentTime(){return this.currentTime}getSecondsLoaded(){return this.secondsLoaded}render(){const e=this.props.url.match(b.MATCH_URL_STREAMABLE)[1],t={width:"100%",height:"100%"};return f.default.createElement("iframe",{ref:this.ref,src:`https://streamable.com/o/${e}`,frameBorder:"0",scrolling:"no",style:t,allow:"encrypted-media; autoplay; fullscreen;"})}}return a(h,"displayName","Streamable"),a(h,"canPlay",b.canPlay.streamable),d}var g=A();const C=T(g),R=x({__proto__:null,default:C},[g]);export{R as S}; +import{r as M,b as D,d as E,g as T}from"./index-DVa6sjDi.js";function x(p,s){for(var u=0;uo[i]})}}}return Object.freeze(Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}))}var d,P;function A(){if(P)return d;P=1;var p=Object.create,s=Object.defineProperty,u=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,i=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,v=(r,e,t)=>e in r?s(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,O=(r,e)=>{for(var t in e)s(r,t,{get:e[t],enumerable:!0})},y=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let l of o(e))!c.call(r,l)&&l!==t&&s(r,l,{get:()=>e[l],enumerable:!(n=u(e,l))||n.enumerable});return r},S=(r,e,t)=>(t=r!=null?p(i(r)):{},y(!r||!r.__esModule?s(t,"default",{value:r,enumerable:!0}):t,r)),j=r=>y(s({},"__esModule",{value:!0}),r),a=(r,e,t)=>(v(r,typeof e!="symbol"?e+"":e,t),t),m={};O(m,{default:()=>h}),d=j(m);var f=S(M()),_=D(),b=E();const L="https://cdn.embed.ly/player-0.1.0.min.js",w="playerjs";class h extends f.Component{constructor(){super(...arguments),a(this,"callPlayer",_.callPlayer),a(this,"duration",null),a(this,"currentTime",null),a(this,"secondsLoaded",null),a(this,"mute",()=>{this.callPlayer("mute")}),a(this,"unmute",()=>{this.callPlayer("unmute")}),a(this,"ref",e=>{this.iframe=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){(0,_.getSDK)(L,w).then(t=>{this.iframe&&(this.player=new t.Player(this.iframe),this.player.setLoop(this.props.loop),this.player.on("ready",this.props.onReady),this.player.on("play",this.props.onPlay),this.player.on("pause",this.props.onPause),this.player.on("seeked",this.props.onSeek),this.player.on("ended",this.props.onEnded),this.player.on("error",this.props.onError),this.player.on("timeupdate",({duration:n,seconds:l})=>{this.duration=n,this.currentTime=l}),this.player.on("buffered",({percent:n})=>{this.duration&&(this.secondsLoaded=this.duration*n)}),this.props.muted&&this.player.mute())},this.props.onError)}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){}seekTo(e,t=!0){this.callPlayer("setCurrentTime",e),t||this.pause()}setVolume(e){this.callPlayer("setVolume",e*100)}setLoop(e){this.callPlayer("setLoop",e)}getDuration(){return this.duration}getCurrentTime(){return this.currentTime}getSecondsLoaded(){return this.secondsLoaded}render(){const e=this.props.url.match(b.MATCH_URL_STREAMABLE)[1],t={width:"100%",height:"100%"};return f.default.createElement("iframe",{ref:this.ref,src:`https://streamable.com/o/${e}`,frameBorder:"0",scrolling:"no",style:t,allow:"encrypted-media; autoplay; fullscreen;"})}}return a(h,"displayName","Streamable"),a(h,"canPlay",b.canPlay.streamable),d}var g=A();const C=T(g),R=x({__proto__:null,default:C},[g]);export{R as S}; diff --git a/server/src/main/resources/static/assets/Twitch-DRtDJx-g.js b/server/conductor-agentspan-server/src/main/resources/static/assets/Twitch-qhW1EEt2.js similarity index 97% rename from server/src/main/resources/static/assets/Twitch-DRtDJx-g.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/Twitch-qhW1EEt2.js index 5de686fa4..2d86315a2 100644 --- a/server/src/main/resources/static/assets/Twitch-DRtDJx-g.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/Twitch-qhW1EEt2.js @@ -1 +1 @@ -import{r as F,b as K,d as V,g as W}from"./index-DiybVIaQ.js";function Y(l,a){for(var p=0;ps[i]})}}}return Object.freeze(Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}))}var f,O;function $(){if(O)return f;O=1;var l=Object.create,a=Object.defineProperty,p=Object.getOwnPropertyDescriptor,s=Object.getOwnPropertyNames,i=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,w=(t,e,r)=>e in t?a(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,b=(t,e)=>{for(var r in e)a(t,r,{get:e[r],enumerable:!0})},v=(t,e,r,_)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of s(e))!c.call(t,o)&&o!==r&&a(t,o,{get:()=>e[o],enumerable:!(_=p(e,o))||_.enumerable});return t},L=(t,e,r)=>(r=t!=null?l(i(t)):{},v(!t||!t.__esModule?a(r,"default",{value:t,enumerable:!0}):r,t)),D=t=>v(a({},"__esModule",{value:!0}),t),n=(t,e,r)=>(w(t,typeof e!="symbol"?e+"":e,r),r),g={};b(g,{default:()=>y}),f=D(g);var m=L(F()),h=K(),d=V();const C="https://player.twitch.tv/js/embed/v1.js",N="Twitch",I="twitch-player-";class y extends m.Component{constructor(){super(...arguments),n(this,"callPlayer",h.callPlayer),n(this,"playerID",this.props.config.playerId||`${I}${(0,h.randomString)()}`),n(this,"mute",()=>{this.callPlayer("setMuted",!0)}),n(this,"unmute",()=>{this.callPlayer("setMuted",!1)})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e,r){const{playsinline:_,onError:o,config:M,controls:S}=this.props,u=d.MATCH_URL_TWITCH_CHANNEL.test(e),P=u?e.match(d.MATCH_URL_TWITCH_CHANNEL)[1]:e.match(d.MATCH_URL_TWITCH_VIDEO)[1];if(r){u?this.player.setChannel(P):this.player.setVideo("v"+P);return}(0,h.getSDK)(C,N).then(E=>{this.player=new E.Player(this.playerID,{video:u?"":P,channel:u?P:"",height:"100%",width:"100%",playsinline:_,autoplay:this.props.playing,muted:this.props.muted,controls:u?!0:S,time:(0,h.parseStartTime)(e),...M.options});const{READY:j,PLAYING:A,PAUSE:R,ENDED:H,ONLINE:x,OFFLINE:U,SEEK:q}=E.Player;this.player.addEventListener(j,this.props.onReady),this.player.addEventListener(A,this.props.onPlay),this.player.addEventListener(R,this.props.onPause),this.player.addEventListener(H,this.props.onEnded),this.player.addEventListener(q,this.props.onSeek),this.player.addEventListener(x,this.props.onLoaded),this.player.addEventListener(U,this.props.onLoaded)},o)}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){this.callPlayer("pause")}seekTo(e,r=!0){this.callPlayer("seek",e),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e)}getDuration(){return this.callPlayer("getDuration")}getCurrentTime(){return this.callPlayer("getCurrentTime")}getSecondsLoaded(){return null}render(){const e={width:"100%",height:"100%"};return m.default.createElement("div",{style:e,id:this.playerID})}}return n(y,"displayName","Twitch"),n(y,"canPlay",d.canPlay.twitch),n(y,"loopOnEnded",!0),f}var T=$();const G=W(T),z=Y({__proto__:null,default:G},[T]);export{z as T}; +import{r as F,b as K,d as V,g as W}from"./index-DVa6sjDi.js";function Y(l,a){for(var p=0;ps[i]})}}}return Object.freeze(Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}))}var f,O;function $(){if(O)return f;O=1;var l=Object.create,a=Object.defineProperty,p=Object.getOwnPropertyDescriptor,s=Object.getOwnPropertyNames,i=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,w=(t,e,r)=>e in t?a(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,b=(t,e)=>{for(var r in e)a(t,r,{get:e[r],enumerable:!0})},v=(t,e,r,_)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of s(e))!c.call(t,o)&&o!==r&&a(t,o,{get:()=>e[o],enumerable:!(_=p(e,o))||_.enumerable});return t},L=(t,e,r)=>(r=t!=null?l(i(t)):{},v(!t||!t.__esModule?a(r,"default",{value:t,enumerable:!0}):r,t)),D=t=>v(a({},"__esModule",{value:!0}),t),n=(t,e,r)=>(w(t,typeof e!="symbol"?e+"":e,r),r),g={};b(g,{default:()=>y}),f=D(g);var m=L(F()),h=K(),d=V();const C="https://player.twitch.tv/js/embed/v1.js",N="Twitch",I="twitch-player-";class y extends m.Component{constructor(){super(...arguments),n(this,"callPlayer",h.callPlayer),n(this,"playerID",this.props.config.playerId||`${I}${(0,h.randomString)()}`),n(this,"mute",()=>{this.callPlayer("setMuted",!0)}),n(this,"unmute",()=>{this.callPlayer("setMuted",!1)})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e,r){const{playsinline:_,onError:o,config:M,controls:S}=this.props,u=d.MATCH_URL_TWITCH_CHANNEL.test(e),P=u?e.match(d.MATCH_URL_TWITCH_CHANNEL)[1]:e.match(d.MATCH_URL_TWITCH_VIDEO)[1];if(r){u?this.player.setChannel(P):this.player.setVideo("v"+P);return}(0,h.getSDK)(C,N).then(E=>{this.player=new E.Player(this.playerID,{video:u?"":P,channel:u?P:"",height:"100%",width:"100%",playsinline:_,autoplay:this.props.playing,muted:this.props.muted,controls:u?!0:S,time:(0,h.parseStartTime)(e),...M.options});const{READY:j,PLAYING:A,PAUSE:R,ENDED:H,ONLINE:x,OFFLINE:U,SEEK:q}=E.Player;this.player.addEventListener(j,this.props.onReady),this.player.addEventListener(A,this.props.onPlay),this.player.addEventListener(R,this.props.onPause),this.player.addEventListener(H,this.props.onEnded),this.player.addEventListener(q,this.props.onSeek),this.player.addEventListener(x,this.props.onLoaded),this.player.addEventListener(U,this.props.onLoaded)},o)}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){this.callPlayer("pause")}seekTo(e,r=!0){this.callPlayer("seek",e),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e)}getDuration(){return this.callPlayer("getDuration")}getCurrentTime(){return this.callPlayer("getCurrentTime")}getSecondsLoaded(){return null}render(){const e={width:"100%",height:"100%"};return m.default.createElement("div",{style:e,id:this.playerID})}}return n(y,"displayName","Twitch"),n(y,"canPlay",d.canPlay.twitch),n(y,"loopOnEnded",!0),f}var T=$();const G=W(T),z=Y({__proto__:null,default:G},[T]);export{z as T}; diff --git a/server/src/main/resources/static/assets/Vidyard-L9nAMazd.js b/server/conductor-agentspan-server/src/main/resources/static/assets/Vidyard-BJO2x236.js similarity index 97% rename from server/src/main/resources/static/assets/Vidyard-L9nAMazd.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/Vidyard-BJO2x236.js index 7bdb5af08..79e2ca4f1 100644 --- a/server/src/main/resources/static/assets/Vidyard-L9nAMazd.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/Vidyard-BJO2x236.js @@ -1 +1 @@ -import{r as x,b as C,d as N,g as q}from"./index-DiybVIaQ.js";function T(l,a){for(var p=0;ps[o]})}}}return Object.freeze(Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}))}var P,O;function k(){if(O)return P;O=1;var l=Object.create,a=Object.defineProperty,p=Object.getOwnPropertyDescriptor,s=Object.getOwnPropertyNames,o=Object.getPrototypeOf,u=Object.prototype.hasOwnProperty,D=(t,e,r)=>e in t?a(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,j=(t,e)=>{for(var r in e)a(t,r,{get:e[r],enumerable:!0})},v=(t,e,r,d)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of s(e))!u.call(t,i)&&i!==r&&a(t,i,{get:()=>e[i],enumerable:!(d=p(e,i))||d.enumerable});return t},w=(t,e,r)=>(r=t!=null?l(o(t)):{},v(!t||!t.__esModule?a(r,"default",{value:t,enumerable:!0}):r,t)),S=t=>v(a({},"__esModule",{value:!0}),t),n=(t,e,r)=>(D(t,typeof e!="symbol"?e+"":e,r),r),g={};j(g,{default:()=>y}),P=S(g);var c=w(x()),m=C(),b=N();const M="https://play.vidyard.com/embed/v4.js",R="VidyardV4",A="onVidyardAPI";class y extends c.Component{constructor(){super(...arguments),n(this,"callPlayer",m.callPlayer),n(this,"mute",()=>{this.setVolume(0)}),n(this,"unmute",()=>{this.props.volume!==null&&this.setVolume(this.props.volume)}),n(this,"ref",e=>{this.container=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){const{playing:r,config:d,onError:i,onDuration:E}=this.props,h=e&&e.match(b.MATCH_URL_VIDYARD)[1];this.player&&this.stop(),(0,m.getSDK)(M,R,A).then(_=>{this.container&&(_.api.addReadyListener((f,L)=>{this.player||(this.player=L,this.player.on("ready",this.props.onReady),this.player.on("play",this.props.onPlay),this.player.on("pause",this.props.onPause),this.player.on("seek",this.props.onSeek),this.player.on("playerComplete",this.props.onEnded))},h),_.api.renderPlayer({uuid:h,container:this.container,autoplay:r?1:0,...d.options}),_.api.getPlayerMetadata(h).then(f=>{this.duration=f.length_in_seconds,E(f.length_in_seconds)}))},i)}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){window.VidyardV4.api.destroyPlayer(this.player)}seekTo(e,r=!0){this.callPlayer("seek",e),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e)}setPlaybackRate(e){this.callPlayer("setPlaybackSpeed",e)}getDuration(){return this.duration}getCurrentTime(){return this.callPlayer("currentTime")}getSecondsLoaded(){return null}render(){const{display:e}=this.props,r={width:"100%",height:"100%",display:e};return c.default.createElement("div",{style:r},c.default.createElement("div",{ref:this.ref}))}}return n(y,"displayName","Vidyard"),n(y,"canPlay",b.canPlay.vidyard),P}var V=k();const K=q(V),B=T({__proto__:null,default:K},[V]);export{B as V}; +import{r as x,b as C,d as N,g as q}from"./index-DVa6sjDi.js";function T(l,a){for(var p=0;ps[o]})}}}return Object.freeze(Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}))}var P,O;function k(){if(O)return P;O=1;var l=Object.create,a=Object.defineProperty,p=Object.getOwnPropertyDescriptor,s=Object.getOwnPropertyNames,o=Object.getPrototypeOf,u=Object.prototype.hasOwnProperty,D=(t,e,r)=>e in t?a(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,j=(t,e)=>{for(var r in e)a(t,r,{get:e[r],enumerable:!0})},v=(t,e,r,d)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of s(e))!u.call(t,i)&&i!==r&&a(t,i,{get:()=>e[i],enumerable:!(d=p(e,i))||d.enumerable});return t},w=(t,e,r)=>(r=t!=null?l(o(t)):{},v(!t||!t.__esModule?a(r,"default",{value:t,enumerable:!0}):r,t)),S=t=>v(a({},"__esModule",{value:!0}),t),n=(t,e,r)=>(D(t,typeof e!="symbol"?e+"":e,r),r),g={};j(g,{default:()=>y}),P=S(g);var c=w(x()),m=C(),b=N();const M="https://play.vidyard.com/embed/v4.js",R="VidyardV4",A="onVidyardAPI";class y extends c.Component{constructor(){super(...arguments),n(this,"callPlayer",m.callPlayer),n(this,"mute",()=>{this.setVolume(0)}),n(this,"unmute",()=>{this.props.volume!==null&&this.setVolume(this.props.volume)}),n(this,"ref",e=>{this.container=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){const{playing:r,config:d,onError:i,onDuration:E}=this.props,h=e&&e.match(b.MATCH_URL_VIDYARD)[1];this.player&&this.stop(),(0,m.getSDK)(M,R,A).then(_=>{this.container&&(_.api.addReadyListener((f,L)=>{this.player||(this.player=L,this.player.on("ready",this.props.onReady),this.player.on("play",this.props.onPlay),this.player.on("pause",this.props.onPause),this.player.on("seek",this.props.onSeek),this.player.on("playerComplete",this.props.onEnded))},h),_.api.renderPlayer({uuid:h,container:this.container,autoplay:r?1:0,...d.options}),_.api.getPlayerMetadata(h).then(f=>{this.duration=f.length_in_seconds,E(f.length_in_seconds)}))},i)}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){window.VidyardV4.api.destroyPlayer(this.player)}seekTo(e,r=!0){this.callPlayer("seek",e),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e)}setPlaybackRate(e){this.callPlayer("setPlaybackSpeed",e)}getDuration(){return this.duration}getCurrentTime(){return this.callPlayer("currentTime")}getSecondsLoaded(){return null}render(){const{display:e}=this.props,r={width:"100%",height:"100%",display:e};return c.default.createElement("div",{style:r},c.default.createElement("div",{ref:this.ref}))}}return n(y,"displayName","Vidyard"),n(y,"canPlay",b.canPlay.vidyard),P}var V=k();const K=q(V),B=T({__proto__:null,default:K},[V]);export{B as V}; diff --git a/server/src/main/resources/static/assets/Vimeo-BKwdn9Eh.js b/server/conductor-agentspan-server/src/main/resources/static/assets/Vimeo-C4XIslfc.js similarity index 98% rename from server/src/main/resources/static/assets/Vimeo-BKwdn9Eh.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/Vimeo-C4XIslfc.js index eea84a761..8f0488dd3 100644 --- a/server/src/main/resources/static/assets/Vimeo-BKwdn9Eh.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/Vimeo-C4XIslfc.js @@ -1 +1 @@ -import{r as L,b as S,d as R,g as k}from"./index-DiybVIaQ.js";function T(p,a){for(var u=0;un[l]})}}}return Object.freeze(Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}))}var d,g;function q(){if(g)return d;g=1;var p=Object.create,a=Object.defineProperty,u=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,l=Object.getPrototypeOf,h=Object.prototype.hasOwnProperty,v=(t,e,r)=>e in t?a(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,O=(t,e)=>{for(var r in e)a(t,r,{get:e[r],enumerable:!0})},f=(t,e,r,y)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of n(e))!h.call(t,i)&&i!==r&&a(t,i,{get:()=>e[i],enumerable:!(y=u(e,i))||y.enumerable});return t},D=(t,e,r)=>(r=t!=null?p(l(t)):{},f(!t||!t.__esModule?a(r,"default",{value:t,enumerable:!0}):r,t)),V=t=>f(a({},"__esModule",{value:!0}),t),s=(t,e,r)=>(v(t,typeof e!="symbol"?e+"":e,r),r),m={};O(m,{default:()=>c}),d=V(m);var _=D(L()),P=S(),w=R();const M="https://player.vimeo.com/api/player.js",j="Vimeo",E=t=>t.replace("/manage/videos","");class c extends _.Component{constructor(){super(...arguments),s(this,"callPlayer",P.callPlayer),s(this,"duration",null),s(this,"currentTime",null),s(this,"secondsLoaded",null),s(this,"mute",()=>{this.setMuted(!0)}),s(this,"unmute",()=>{this.setMuted(!1)}),s(this,"ref",e=>{this.container=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){this.duration=null,(0,P.getSDK)(M,j).then(r=>{if(!this.container)return;const{playerOptions:y,title:i}=this.props.config;this.player=new r.Player(this.container,{url:E(e),autoplay:this.props.playing,muted:this.props.muted,loop:this.props.loop,playsinline:this.props.playsinline,controls:this.props.controls,...y}),this.player.ready().then(()=>{const o=this.container.querySelector("iframe");o.style.width="100%",o.style.height="100%",i&&(o.title=i)}).catch(this.props.onError),this.player.on("loaded",()=>{this.props.onReady(),this.refreshDuration()}),this.player.on("play",()=>{this.props.onPlay(),this.refreshDuration()}),this.player.on("pause",this.props.onPause),this.player.on("seeked",o=>this.props.onSeek(o.seconds)),this.player.on("ended",this.props.onEnded),this.player.on("error",this.props.onError),this.player.on("timeupdate",({seconds:o})=>{this.currentTime=o}),this.player.on("progress",({seconds:o})=>{this.secondsLoaded=o}),this.player.on("bufferstart",this.props.onBuffer),this.player.on("bufferend",this.props.onBufferEnd),this.player.on("playbackratechange",o=>this.props.onPlaybackRateChange(o.playbackRate))},this.props.onError)}refreshDuration(){this.player.getDuration().then(e=>{this.duration=e})}play(){const e=this.callPlayer("play");e&&e.catch(this.props.onError)}pause(){this.callPlayer("pause")}stop(){this.callPlayer("unload")}seekTo(e,r=!0){this.callPlayer("setCurrentTime",e),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e)}setMuted(e){this.callPlayer("setMuted",e)}setLoop(e){this.callPlayer("setLoop",e)}setPlaybackRate(e){this.callPlayer("setPlaybackRate",e)}getDuration(){return this.duration}getCurrentTime(){return this.currentTime}getSecondsLoaded(){return this.secondsLoaded}render(){const{display:e}=this.props,r={width:"100%",height:"100%",overflow:"hidden",display:e};return _.default.createElement("div",{key:this.props.url,ref:this.ref,style:r})}}return s(c,"displayName","Vimeo"),s(c,"canPlay",w.canPlay.vimeo),s(c,"forceLoad",!0),d}var b=q();const x=k(b),N=T({__proto__:null,default:x},[b]);export{N as V}; +import{r as L,b as S,d as R,g as k}from"./index-DVa6sjDi.js";function T(p,a){for(var u=0;un[l]})}}}return Object.freeze(Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}))}var d,g;function q(){if(g)return d;g=1;var p=Object.create,a=Object.defineProperty,u=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,l=Object.getPrototypeOf,h=Object.prototype.hasOwnProperty,v=(t,e,r)=>e in t?a(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,O=(t,e)=>{for(var r in e)a(t,r,{get:e[r],enumerable:!0})},f=(t,e,r,y)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of n(e))!h.call(t,i)&&i!==r&&a(t,i,{get:()=>e[i],enumerable:!(y=u(e,i))||y.enumerable});return t},D=(t,e,r)=>(r=t!=null?p(l(t)):{},f(!t||!t.__esModule?a(r,"default",{value:t,enumerable:!0}):r,t)),V=t=>f(a({},"__esModule",{value:!0}),t),s=(t,e,r)=>(v(t,typeof e!="symbol"?e+"":e,r),r),m={};O(m,{default:()=>c}),d=V(m);var _=D(L()),P=S(),w=R();const M="https://player.vimeo.com/api/player.js",j="Vimeo",E=t=>t.replace("/manage/videos","");class c extends _.Component{constructor(){super(...arguments),s(this,"callPlayer",P.callPlayer),s(this,"duration",null),s(this,"currentTime",null),s(this,"secondsLoaded",null),s(this,"mute",()=>{this.setMuted(!0)}),s(this,"unmute",()=>{this.setMuted(!1)}),s(this,"ref",e=>{this.container=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){this.duration=null,(0,P.getSDK)(M,j).then(r=>{if(!this.container)return;const{playerOptions:y,title:i}=this.props.config;this.player=new r.Player(this.container,{url:E(e),autoplay:this.props.playing,muted:this.props.muted,loop:this.props.loop,playsinline:this.props.playsinline,controls:this.props.controls,...y}),this.player.ready().then(()=>{const o=this.container.querySelector("iframe");o.style.width="100%",o.style.height="100%",i&&(o.title=i)}).catch(this.props.onError),this.player.on("loaded",()=>{this.props.onReady(),this.refreshDuration()}),this.player.on("play",()=>{this.props.onPlay(),this.refreshDuration()}),this.player.on("pause",this.props.onPause),this.player.on("seeked",o=>this.props.onSeek(o.seconds)),this.player.on("ended",this.props.onEnded),this.player.on("error",this.props.onError),this.player.on("timeupdate",({seconds:o})=>{this.currentTime=o}),this.player.on("progress",({seconds:o})=>{this.secondsLoaded=o}),this.player.on("bufferstart",this.props.onBuffer),this.player.on("bufferend",this.props.onBufferEnd),this.player.on("playbackratechange",o=>this.props.onPlaybackRateChange(o.playbackRate))},this.props.onError)}refreshDuration(){this.player.getDuration().then(e=>{this.duration=e})}play(){const e=this.callPlayer("play");e&&e.catch(this.props.onError)}pause(){this.callPlayer("pause")}stop(){this.callPlayer("unload")}seekTo(e,r=!0){this.callPlayer("setCurrentTime",e),r||this.pause()}setVolume(e){this.callPlayer("setVolume",e)}setMuted(e){this.callPlayer("setMuted",e)}setLoop(e){this.callPlayer("setLoop",e)}setPlaybackRate(e){this.callPlayer("setPlaybackRate",e)}getDuration(){return this.duration}getCurrentTime(){return this.currentTime}getSecondsLoaded(){return this.secondsLoaded}render(){const{display:e}=this.props,r={width:"100%",height:"100%",overflow:"hidden",display:e};return _.default.createElement("div",{key:this.props.url,ref:this.ref,style:r})}}return s(c,"displayName","Vimeo"),s(c,"canPlay",w.canPlay.vimeo),s(c,"forceLoad",!0),d}var b=q();const x=k(b),N=T({__proto__:null,default:x},[b]);export{N as V}; diff --git a/server/src/main/resources/static/assets/Wistia-DaMqyZOi.js b/server/conductor-agentspan-server/src/main/resources/static/assets/Wistia-DSoqhXcd.js similarity index 98% rename from server/src/main/resources/static/assets/Wistia-DaMqyZOi.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/Wistia-DSoqhXcd.js index a1fb3e3cb..e4c06fb23 100644 --- a/server/src/main/resources/static/assets/Wistia-DaMqyZOi.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/Wistia-DSoqhXcd.js @@ -1 +1 @@ -import{r as I,b as M,d as x,g as A}from"./index-DiybVIaQ.js";function L(p,i){for(var u=0;un[o]})}}}return Object.freeze(Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}))}var P,v;function N(){if(v)return P;v=1;var p=Object.create,i=Object.defineProperty,u=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,o=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,O=(t,e,a)=>e in t?i(t,e,{enumerable:!0,configurable:!0,writable:!0,value:a}):t[e]=a,C=(t,e)=>{for(var a in e)i(t,a,{get:e[a],enumerable:!0})},b=(t,e,a,l)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of n(e))!c.call(t,r)&&r!==a&&i(t,r,{get:()=>e[r],enumerable:!(l=u(e,r))||l.enumerable});return t},k=(t,e,a)=>(a=t!=null?p(o(t)):{},b(!t||!t.__esModule?i(a,"default",{value:t,enumerable:!0}):a,t)),R=t=>b(i({},"__esModule",{value:!0}),t),s=(t,e,a)=>(O(t,typeof e!="symbol"?e+"":e,a),a),f={};C(f,{default:()=>h}),P=R(f);var g=k(I()),y=M(),m=x();const D="https://fast.wistia.com/assets/external/E-v1.js",E="Wistia",S="wistia-player-";class h extends g.Component{constructor(){super(...arguments),s(this,"callPlayer",y.callPlayer),s(this,"playerID",this.props.config.playerId||`${S}${(0,y.randomString)()}`),s(this,"onPlay",(...e)=>this.props.onPlay(...e)),s(this,"onPause",(...e)=>this.props.onPause(...e)),s(this,"onSeek",(...e)=>this.props.onSeek(...e)),s(this,"onEnded",(...e)=>this.props.onEnded(...e)),s(this,"onPlaybackRateChange",(...e)=>this.props.onPlaybackRateChange(...e)),s(this,"mute",()=>{this.callPlayer("mute")}),s(this,"unmute",()=>{this.callPlayer("unmute")})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){const{playing:a,muted:l,controls:r,onReady:W,config:d,onError:j}=this.props;(0,y.getSDK)(D,E).then(q=>{d.customControls&&d.customControls.forEach(_=>q.defineControl(_)),window._wq=window._wq||[],window._wq.push({id:this.playerID,options:{autoPlay:a,silentAutoPlay:"allow",muted:l,controlsVisibleOnLoad:r,fullscreenButton:r,playbar:r,playbackRateControl:r,qualityControl:r,volumeControl:r,settingsControl:r,smallPlayButton:r,...d.options},onReady:_=>{this.player=_,this.unbind(),this.player.bind("play",this.onPlay),this.player.bind("pause",this.onPause),this.player.bind("seek",this.onSeek),this.player.bind("end",this.onEnded),this.player.bind("playbackratechange",this.onPlaybackRateChange),W()}})},j)}unbind(){this.player.unbind("play",this.onPlay),this.player.unbind("pause",this.onPause),this.player.unbind("seek",this.onSeek),this.player.unbind("end",this.onEnded),this.player.unbind("playbackratechange",this.onPlaybackRateChange)}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){this.unbind(),this.callPlayer("remove")}seekTo(e,a=!0){this.callPlayer("time",e),a||this.pause()}setVolume(e){this.callPlayer("volume",e)}setPlaybackRate(e){this.callPlayer("playbackRate",e)}getDuration(){return this.callPlayer("duration")}getCurrentTime(){return this.callPlayer("time")}getSecondsLoaded(){return null}render(){const{url:e}=this.props,a=e&&e.match(m.MATCH_URL_WISTIA)[1],l=`wistia_embed wistia_async_${a}`,r={width:"100%",height:"100%"};return g.default.createElement("div",{id:this.playerID,key:a,className:l,style:r})}}return s(h,"displayName","Wistia"),s(h,"canPlay",m.canPlay.wistia),s(h,"loopOnEnded",!0),P}var w=N();const T=A(w),B=L({__proto__:null,default:T},[w]);export{B as W}; +import{r as I,b as M,d as x,g as A}from"./index-DVa6sjDi.js";function L(p,i){for(var u=0;un[o]})}}}return Object.freeze(Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}))}var P,v;function N(){if(v)return P;v=1;var p=Object.create,i=Object.defineProperty,u=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,o=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,O=(t,e,a)=>e in t?i(t,e,{enumerable:!0,configurable:!0,writable:!0,value:a}):t[e]=a,C=(t,e)=>{for(var a in e)i(t,a,{get:e[a],enumerable:!0})},b=(t,e,a,l)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of n(e))!c.call(t,r)&&r!==a&&i(t,r,{get:()=>e[r],enumerable:!(l=u(e,r))||l.enumerable});return t},k=(t,e,a)=>(a=t!=null?p(o(t)):{},b(!t||!t.__esModule?i(a,"default",{value:t,enumerable:!0}):a,t)),R=t=>b(i({},"__esModule",{value:!0}),t),s=(t,e,a)=>(O(t,typeof e!="symbol"?e+"":e,a),a),f={};C(f,{default:()=>h}),P=R(f);var g=k(I()),y=M(),m=x();const D="https://fast.wistia.com/assets/external/E-v1.js",E="Wistia",S="wistia-player-";class h extends g.Component{constructor(){super(...arguments),s(this,"callPlayer",y.callPlayer),s(this,"playerID",this.props.config.playerId||`${S}${(0,y.randomString)()}`),s(this,"onPlay",(...e)=>this.props.onPlay(...e)),s(this,"onPause",(...e)=>this.props.onPause(...e)),s(this,"onSeek",(...e)=>this.props.onSeek(...e)),s(this,"onEnded",(...e)=>this.props.onEnded(...e)),s(this,"onPlaybackRateChange",(...e)=>this.props.onPlaybackRateChange(...e)),s(this,"mute",()=>{this.callPlayer("mute")}),s(this,"unmute",()=>{this.callPlayer("unmute")})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}load(e){const{playing:a,muted:l,controls:r,onReady:W,config:d,onError:j}=this.props;(0,y.getSDK)(D,E).then(q=>{d.customControls&&d.customControls.forEach(_=>q.defineControl(_)),window._wq=window._wq||[],window._wq.push({id:this.playerID,options:{autoPlay:a,silentAutoPlay:"allow",muted:l,controlsVisibleOnLoad:r,fullscreenButton:r,playbar:r,playbackRateControl:r,qualityControl:r,volumeControl:r,settingsControl:r,smallPlayButton:r,...d.options},onReady:_=>{this.player=_,this.unbind(),this.player.bind("play",this.onPlay),this.player.bind("pause",this.onPause),this.player.bind("seek",this.onSeek),this.player.bind("end",this.onEnded),this.player.bind("playbackratechange",this.onPlaybackRateChange),W()}})},j)}unbind(){this.player.unbind("play",this.onPlay),this.player.unbind("pause",this.onPause),this.player.unbind("seek",this.onSeek),this.player.unbind("end",this.onEnded),this.player.unbind("playbackratechange",this.onPlaybackRateChange)}play(){this.callPlayer("play")}pause(){this.callPlayer("pause")}stop(){this.unbind(),this.callPlayer("remove")}seekTo(e,a=!0){this.callPlayer("time",e),a||this.pause()}setVolume(e){this.callPlayer("volume",e)}setPlaybackRate(e){this.callPlayer("playbackRate",e)}getDuration(){return this.callPlayer("duration")}getCurrentTime(){return this.callPlayer("time")}getSecondsLoaded(){return null}render(){const{url:e}=this.props,a=e&&e.match(m.MATCH_URL_WISTIA)[1],l=`wistia_embed wistia_async_${a}`,r={width:"100%",height:"100%"};return g.default.createElement("div",{id:this.playerID,key:a,className:l,style:r})}}return s(h,"displayName","Wistia"),s(h,"canPlay",m.canPlay.wistia),s(h,"loopOnEnded",!0),P}var w=N();const T=A(w),B=L({__proto__:null,default:T},[w]);export{B as W}; diff --git a/server/src/main/resources/static/assets/YouTube-BR2c3EbR.js b/server/conductor-agentspan-server/src/main/resources/static/assets/YouTube-CLOSIIy_.js similarity index 98% rename from server/src/main/resources/static/assets/YouTube-BR2c3EbR.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/YouTube-CLOSIIy_.js index 17e1e6821..fa75ee16e 100644 --- a/server/src/main/resources/static/assets/YouTube-BR2c3EbR.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/YouTube-CLOSIIy_.js @@ -1 +1 @@ -import{r as G,b as z,d as Q,g as Z}from"./index-DiybVIaQ.js";function J(y,o){for(var d=0;dn[i]})}}}return Object.freeze(Object.defineProperty(y,Symbol.toStringTag,{value:"Module"}))}var E,Y;function $(){if(Y)return E;Y=1;var y=Object.create,o=Object.defineProperty,d=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,i=Object.getPrototypeOf,h=Object.prototype.hasOwnProperty,L=(a,e,t)=>e in a?o(a,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):a[e]=t,M=(a,e)=>{for(var t in e)o(a,t,{get:e[t],enumerable:!0})},A=(a,e,t,p)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of n(e))!h.call(a,r)&&r!==t&&o(a,r,{get:()=>e[r],enumerable:!(p=d(e,r))||p.enumerable});return a},k=(a,e,t)=>(t=a!=null?y(i(a)):{},A(!a||!a.__esModule?o(t,"default",{value:a,enumerable:!0}):t,a)),N=a=>A(o({},"__esModule",{value:!0}),a),s=(a,e,t)=>(L(a,typeof e!="symbol"?e+"":e,t),t),C={};M(C,{default:()=>O}),E=N(C);var m=k(G()),c=z(),R=Q();const j="https://www.youtube.com/iframe_api",U="YT",V="onYouTubeIframeAPIReady",P=/[?&](?:list|channel)=([a-zA-Z0-9_-]+)/,T=/user\/([a-zA-Z0-9_-]+)\/?/,B=/youtube-nocookie\.com/,x="https://www.youtube-nocookie.com";class O extends m.Component{constructor(){super(...arguments),s(this,"callPlayer",c.callPlayer),s(this,"parsePlaylist",e=>{if(e instanceof Array)return{listType:"playlist",playlist:e.map(this.getID).join(",")};if(P.test(e)){const[,t]=e.match(P);return{listType:"playlist",list:t.replace(/^UC/,"UU")}}if(T.test(e)){const[,t]=e.match(T);return{listType:"user_uploads",list:t}}return{}}),s(this,"onStateChange",e=>{const{data:t}=e,{onPlay:p,onPause:r,onBuffer:v,onBufferEnd:w,onEnded:S,onReady:D,loop:_,config:{playerVars:u,onUnstarted:g}}=this.props,{UNSTARTED:b,PLAYING:f,PAUSED:l,BUFFERING:K,ENDED:q,CUED:F}=window[U].PlayerState;if(t===b&&g(),t===f&&(p(),w()),t===l&&r(),t===K&&v(),t===q){const H=!!this.callPlayer("getPlaylist");_&&!H&&(u.start?this.seekTo(u.start):this.play()),S()}t===F&&D()}),s(this,"mute",()=>{this.callPlayer("mute")}),s(this,"unmute",()=>{this.callPlayer("unMute")}),s(this,"ref",e=>{this.container=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}getID(e){return!e||e instanceof Array||P.test(e)?null:e.match(R.MATCH_URL_YOUTUBE)[1]}load(e,t){const{playing:p,muted:r,playsinline:v,controls:w,loop:S,config:D,onError:_}=this.props,{playerVars:u,embedOptions:g}=D,b=this.getID(e);if(t){if(P.test(e)||T.test(e)||e instanceof Array){this.player.loadPlaylist(this.parsePlaylist(e));return}this.player.cueVideoById({videoId:b,startSeconds:(0,c.parseStartTime)(e)||u.start,endSeconds:(0,c.parseEndTime)(e)||u.end});return}(0,c.getSDK)(j,U,V,f=>f.loaded).then(f=>{this.container&&(this.player=new f.Player(this.container,{width:"100%",height:"100%",videoId:b,playerVars:{autoplay:p?1:0,mute:r?1:0,controls:w?1:0,start:(0,c.parseStartTime)(e),end:(0,c.parseEndTime)(e),origin:window.location.origin,playsinline:v?1:0,...this.parsePlaylist(e),...u},events:{onReady:()=>{S&&this.player.setLoop(!0),this.props.onReady()},onPlaybackRateChange:l=>this.props.onPlaybackRateChange(l.data),onPlaybackQualityChange:l=>this.props.onPlaybackQualityChange(l),onStateChange:this.onStateChange,onError:l=>_(l.data)},host:B.test(e)?x:void 0,...g}))},_),g.events&&console.warn("Using `embedOptions.events` will likely break things. Use ReactPlayer’s callback props instead, eg onReady, onPlay, onPause")}play(){this.callPlayer("playVideo")}pause(){this.callPlayer("pauseVideo")}stop(){document.body.contains(this.callPlayer("getIframe"))&&this.callPlayer("stopVideo")}seekTo(e,t=!1){this.callPlayer("seekTo",e),!t&&!this.props.playing&&this.pause()}setVolume(e){this.callPlayer("setVolume",e*100)}setPlaybackRate(e){this.callPlayer("setPlaybackRate",e)}setLoop(e){this.callPlayer("setLoop",e)}getDuration(){return this.callPlayer("getDuration")}getCurrentTime(){return this.callPlayer("getCurrentTime")}getSecondsLoaded(){return this.callPlayer("getVideoLoadedFraction")*this.getDuration()}render(){const{display:e}=this.props,t={width:"100%",height:"100%",display:e};return m.default.createElement("div",{style:t},m.default.createElement("div",{ref:this.ref}))}}return s(O,"displayName","YouTube"),s(O,"canPlay",R.canPlay.youtube),E}var I=$();const W=Z(I),ee=J({__proto__:null,default:W},[I]);export{ee as Y}; +import{r as G,b as z,d as Q,g as Z}from"./index-DVa6sjDi.js";function J(y,o){for(var d=0;dn[i]})}}}return Object.freeze(Object.defineProperty(y,Symbol.toStringTag,{value:"Module"}))}var E,Y;function $(){if(Y)return E;Y=1;var y=Object.create,o=Object.defineProperty,d=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,i=Object.getPrototypeOf,h=Object.prototype.hasOwnProperty,L=(a,e,t)=>e in a?o(a,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):a[e]=t,M=(a,e)=>{for(var t in e)o(a,t,{get:e[t],enumerable:!0})},A=(a,e,t,p)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of n(e))!h.call(a,r)&&r!==t&&o(a,r,{get:()=>e[r],enumerable:!(p=d(e,r))||p.enumerable});return a},k=(a,e,t)=>(t=a!=null?y(i(a)):{},A(!a||!a.__esModule?o(t,"default",{value:a,enumerable:!0}):t,a)),N=a=>A(o({},"__esModule",{value:!0}),a),s=(a,e,t)=>(L(a,typeof e!="symbol"?e+"":e,t),t),C={};M(C,{default:()=>O}),E=N(C);var m=k(G()),c=z(),R=Q();const j="https://www.youtube.com/iframe_api",U="YT",V="onYouTubeIframeAPIReady",P=/[?&](?:list|channel)=([a-zA-Z0-9_-]+)/,T=/user\/([a-zA-Z0-9_-]+)\/?/,B=/youtube-nocookie\.com/,x="https://www.youtube-nocookie.com";class O extends m.Component{constructor(){super(...arguments),s(this,"callPlayer",c.callPlayer),s(this,"parsePlaylist",e=>{if(e instanceof Array)return{listType:"playlist",playlist:e.map(this.getID).join(",")};if(P.test(e)){const[,t]=e.match(P);return{listType:"playlist",list:t.replace(/^UC/,"UU")}}if(T.test(e)){const[,t]=e.match(T);return{listType:"user_uploads",list:t}}return{}}),s(this,"onStateChange",e=>{const{data:t}=e,{onPlay:p,onPause:r,onBuffer:v,onBufferEnd:w,onEnded:S,onReady:D,loop:_,config:{playerVars:u,onUnstarted:g}}=this.props,{UNSTARTED:b,PLAYING:f,PAUSED:l,BUFFERING:K,ENDED:q,CUED:F}=window[U].PlayerState;if(t===b&&g(),t===f&&(p(),w()),t===l&&r(),t===K&&v(),t===q){const H=!!this.callPlayer("getPlaylist");_&&!H&&(u.start?this.seekTo(u.start):this.play()),S()}t===F&&D()}),s(this,"mute",()=>{this.callPlayer("mute")}),s(this,"unmute",()=>{this.callPlayer("unMute")}),s(this,"ref",e=>{this.container=e})}componentDidMount(){this.props.onMount&&this.props.onMount(this)}getID(e){return!e||e instanceof Array||P.test(e)?null:e.match(R.MATCH_URL_YOUTUBE)[1]}load(e,t){const{playing:p,muted:r,playsinline:v,controls:w,loop:S,config:D,onError:_}=this.props,{playerVars:u,embedOptions:g}=D,b=this.getID(e);if(t){if(P.test(e)||T.test(e)||e instanceof Array){this.player.loadPlaylist(this.parsePlaylist(e));return}this.player.cueVideoById({videoId:b,startSeconds:(0,c.parseStartTime)(e)||u.start,endSeconds:(0,c.parseEndTime)(e)||u.end});return}(0,c.getSDK)(j,U,V,f=>f.loaded).then(f=>{this.container&&(this.player=new f.Player(this.container,{width:"100%",height:"100%",videoId:b,playerVars:{autoplay:p?1:0,mute:r?1:0,controls:w?1:0,start:(0,c.parseStartTime)(e),end:(0,c.parseEndTime)(e),origin:window.location.origin,playsinline:v?1:0,...this.parsePlaylist(e),...u},events:{onReady:()=>{S&&this.player.setLoop(!0),this.props.onReady()},onPlaybackRateChange:l=>this.props.onPlaybackRateChange(l.data),onPlaybackQualityChange:l=>this.props.onPlaybackQualityChange(l),onStateChange:this.onStateChange,onError:l=>_(l.data)},host:B.test(e)?x:void 0,...g}))},_),g.events&&console.warn("Using `embedOptions.events` will likely break things. Use ReactPlayer’s callback props instead, eg onReady, onPlay, onPause")}play(){this.callPlayer("playVideo")}pause(){this.callPlayer("pauseVideo")}stop(){document.body.contains(this.callPlayer("getIframe"))&&this.callPlayer("stopVideo")}seekTo(e,t=!1){this.callPlayer("seekTo",e),!t&&!this.props.playing&&this.pause()}setVolume(e){this.callPlayer("setVolume",e*100)}setPlaybackRate(e){this.callPlayer("setPlaybackRate",e)}setLoop(e){this.callPlayer("setLoop",e)}getDuration(){return this.callPlayer("getDuration")}getCurrentTime(){return this.callPlayer("getCurrentTime")}getSecondsLoaded(){return this.callPlayer("getVideoLoadedFraction")*this.getDuration()}render(){const{display:e}=this.props,t={width:"100%",height:"100%",display:e};return m.default.createElement("div",{style:t},m.default.createElement("div",{ref:this.ref}))}}return s(O,"displayName","YouTube"),s(O,"canPlay",R.canPlay.youtube),E}var I=$();const W=Z(I),ee=J({__proto__:null,default:W},[I]);export{ee as Y}; diff --git a/server/src/main/resources/static/assets/abap-DLDM7-KI.js b/server/conductor-agentspan-server/src/main/resources/static/assets/abap-DLDM7-KI.js similarity index 100% rename from server/src/main/resources/static/assets/abap-DLDM7-KI.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/abap-DLDM7-KI.js diff --git a/server/src/main/resources/static/assets/apex-DNDY2TF8.js b/server/conductor-agentspan-server/src/main/resources/static/assets/apex-DNDY2TF8.js similarity index 100% rename from server/src/main/resources/static/assets/apex-DNDY2TF8.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/apex-DNDY2TF8.js diff --git a/server/src/main/resources/static/assets/azcli-Y6nb8tq_.js b/server/conductor-agentspan-server/src/main/resources/static/assets/azcli-Y6nb8tq_.js similarity index 100% rename from server/src/main/resources/static/assets/azcli-Y6nb8tq_.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/azcli-Y6nb8tq_.js diff --git a/server/src/main/resources/static/assets/bat-BwHxbl9M.js b/server/conductor-agentspan-server/src/main/resources/static/assets/bat-BwHxbl9M.js similarity index 100% rename from server/src/main/resources/static/assets/bat-BwHxbl9M.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/bat-BwHxbl9M.js diff --git a/server/src/main/resources/static/assets/bicep-CFznDFnq.js b/server/conductor-agentspan-server/src/main/resources/static/assets/bicep-CFznDFnq.js similarity index 100% rename from server/src/main/resources/static/assets/bicep-CFznDFnq.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/bicep-CFznDFnq.js diff --git a/server/src/main/resources/static/assets/cameligo-Bf6VGUru.js b/server/conductor-agentspan-server/src/main/resources/static/assets/cameligo-Bf6VGUru.js similarity index 100% rename from server/src/main/resources/static/assets/cameligo-Bf6VGUru.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/cameligo-Bf6VGUru.js diff --git a/server/src/main/resources/static/assets/clojure-Dnu-v4kV.js b/server/conductor-agentspan-server/src/main/resources/static/assets/clojure-Dnu-v4kV.js similarity index 100% rename from server/src/main/resources/static/assets/clojure-Dnu-v4kV.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/clojure-Dnu-v4kV.js diff --git a/server/src/main/resources/static/assets/codicon-ngg6Pgfi.ttf b/server/conductor-agentspan-server/src/main/resources/static/assets/codicon-ngg6Pgfi.ttf similarity index 100% rename from server/src/main/resources/static/assets/codicon-ngg6Pgfi.ttf rename to server/conductor-agentspan-server/src/main/resources/static/assets/codicon-ngg6Pgfi.ttf diff --git a/server/src/main/resources/static/assets/coffee-Bd8akH9Z.js b/server/conductor-agentspan-server/src/main/resources/static/assets/coffee-Bd8akH9Z.js similarity index 100% rename from server/src/main/resources/static/assets/coffee-Bd8akH9Z.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/coffee-Bd8akH9Z.js diff --git a/server/src/main/resources/static/assets/cpp-BbWJElDN.js b/server/conductor-agentspan-server/src/main/resources/static/assets/cpp-BbWJElDN.js similarity index 100% rename from server/src/main/resources/static/assets/cpp-BbWJElDN.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/cpp-BbWJElDN.js diff --git a/server/src/main/resources/static/assets/csharp-Co3qMtFm.js b/server/conductor-agentspan-server/src/main/resources/static/assets/csharp-Co3qMtFm.js similarity index 100% rename from server/src/main/resources/static/assets/csharp-Co3qMtFm.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/csharp-Co3qMtFm.js diff --git a/server/src/main/resources/static/assets/csp-D-4FJmMZ.js b/server/conductor-agentspan-server/src/main/resources/static/assets/csp-D-4FJmMZ.js similarity index 100% rename from server/src/main/resources/static/assets/csp-D-4FJmMZ.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/csp-D-4FJmMZ.js diff --git a/server/src/main/resources/static/assets/css-DdJfP1eB.js b/server/conductor-agentspan-server/src/main/resources/static/assets/css-DdJfP1eB.js similarity index 100% rename from server/src/main/resources/static/assets/css-DdJfP1eB.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/css-DdJfP1eB.js diff --git a/server/src/main/resources/static/assets/css.worker-DBVD8oXr.js b/server/conductor-agentspan-server/src/main/resources/static/assets/css.worker-DBVD8oXr.js similarity index 100% rename from server/src/main/resources/static/assets/css.worker-DBVD8oXr.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/css.worker-DBVD8oXr.js diff --git a/server/src/main/resources/static/assets/cssMode-DczLDKZ6.js b/server/conductor-agentspan-server/src/main/resources/static/assets/cssMode-Bg4Vg_j9.js similarity index 91% rename from server/src/main/resources/static/assets/cssMode-DczLDKZ6.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/cssMode-Bg4Vg_j9.js index df6034455..22e8f7b4a 100644 --- a/server/src/main/resources/static/assets/cssMode-DczLDKZ6.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/cssMode-Bg4Vg_j9.js @@ -1 +1 @@ -import{c as h,l as s}from"./index-DiybVIaQ.js";import{C as c,H as u,D as p,a as m,R as f,b as _,c as w,d as k,F as v,e as D,S as P,f as R,g as I}from"./lspLanguageFeatures-DRGgTbma.js";import{h as b,i as H,j as y,t as U,k as T}from"./lspLanguageFeatures-DRGgTbma.js";const C=120*1e3;class A{constructor(n){this._defaults=n,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&(this._worker.dispose(),this._worker=null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){if(!this._worker)return;Date.now()-this._lastUsedTime>C&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||(this._worker=h({moduleId:"vs/language/css/cssWorker",createWorker:()=>new Worker(new URL("/ui/assets/css.worker-DBVD8oXr.js",import.meta.url),{type:"module"}),label:this._defaults.languageId,createData:{options:this._defaults.options,languageId:this._defaults.languageId}}),this._client=this._worker.getProxy()),this._client}getLanguageServiceWorker(...n){let e;return this._getClient().then(a=>{e=a}).then(a=>{if(this._worker)return this._worker.withSyncedResources(n)}).then(a=>e)}}function F(o){const n=[],e=[],a=new A(o);n.push(a);const r=(...t)=>a.getLanguageServiceWorker(...t);function l(){const{languageId:t,modeConfiguration:i}=o;g(e),i.completionItems&&e.push(s.registerCompletionItemProvider(t,new c(r,["/","-",":"]))),i.hovers&&e.push(s.registerHoverProvider(t,new u(r))),i.documentHighlights&&e.push(s.registerDocumentHighlightProvider(t,new p(r))),i.definitions&&e.push(s.registerDefinitionProvider(t,new m(r))),i.references&&e.push(s.registerReferenceProvider(t,new f(r))),i.documentSymbols&&e.push(s.registerDocumentSymbolProvider(t,new _(r))),i.rename&&e.push(s.registerRenameProvider(t,new w(r))),i.colors&&e.push(s.registerColorProvider(t,new k(r))),i.foldingRanges&&e.push(s.registerFoldingRangeProvider(t,new v(r))),i.diagnostics&&e.push(new D(t,r,o.onDidChange)),i.selectionRanges&&e.push(s.registerSelectionRangeProvider(t,new P(r))),i.documentFormattingEdits&&e.push(s.registerDocumentFormattingEditProvider(t,new R(r))),i.documentRangeFormattingEdits&&e.push(s.registerDocumentRangeFormattingEditProvider(t,new I(r)))}return l(),n.push(d(e)),d(n)}function d(o){return{dispose:()=>g(o)}}function g(o){for(;o.length;)o.pop().dispose()}export{c as CompletionAdapter,m as DefinitionAdapter,D as DiagnosticsAdapter,k as DocumentColorAdapter,R as DocumentFormattingEditProvider,p as DocumentHighlightAdapter,b as DocumentLinkAdapter,I as DocumentRangeFormattingEditProvider,_ as DocumentSymbolAdapter,v as FoldingRangeAdapter,u as HoverAdapter,f as ReferenceAdapter,w as RenameAdapter,P as SelectionRangeAdapter,A as WorkerManager,H as fromPosition,y as fromRange,F as setupMode,U as toRange,T as toTextEdit}; +import{c as h,l as s}from"./index-DVa6sjDi.js";import{C as c,H as u,D as p,a as m,R as f,b as _,c as w,d as k,F as v,e as D,S as P,f as R,g as I}from"./lspLanguageFeatures-WUkMtvXB.js";import{h as b,i as H,j as y,t as U,k as T}from"./lspLanguageFeatures-WUkMtvXB.js";const C=120*1e3;class A{constructor(n){this._defaults=n,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&(this._worker.dispose(),this._worker=null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){if(!this._worker)return;Date.now()-this._lastUsedTime>C&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||(this._worker=h({moduleId:"vs/language/css/cssWorker",createWorker:()=>new Worker(new URL("/ui/assets/css.worker-DBVD8oXr.js",import.meta.url),{type:"module"}),label:this._defaults.languageId,createData:{options:this._defaults.options,languageId:this._defaults.languageId}}),this._client=this._worker.getProxy()),this._client}getLanguageServiceWorker(...n){let e;return this._getClient().then(a=>{e=a}).then(a=>{if(this._worker)return this._worker.withSyncedResources(n)}).then(a=>e)}}function F(o){const n=[],e=[],a=new A(o);n.push(a);const r=(...t)=>a.getLanguageServiceWorker(...t);function l(){const{languageId:t,modeConfiguration:i}=o;g(e),i.completionItems&&e.push(s.registerCompletionItemProvider(t,new c(r,["/","-",":"]))),i.hovers&&e.push(s.registerHoverProvider(t,new u(r))),i.documentHighlights&&e.push(s.registerDocumentHighlightProvider(t,new p(r))),i.definitions&&e.push(s.registerDefinitionProvider(t,new m(r))),i.references&&e.push(s.registerReferenceProvider(t,new f(r))),i.documentSymbols&&e.push(s.registerDocumentSymbolProvider(t,new _(r))),i.rename&&e.push(s.registerRenameProvider(t,new w(r))),i.colors&&e.push(s.registerColorProvider(t,new k(r))),i.foldingRanges&&e.push(s.registerFoldingRangeProvider(t,new v(r))),i.diagnostics&&e.push(new D(t,r,o.onDidChange)),i.selectionRanges&&e.push(s.registerSelectionRangeProvider(t,new P(r))),i.documentFormattingEdits&&e.push(s.registerDocumentFormattingEditProvider(t,new R(r))),i.documentRangeFormattingEdits&&e.push(s.registerDocumentRangeFormattingEditProvider(t,new I(r)))}return l(),n.push(d(e)),d(n)}function d(o){return{dispose:()=>g(o)}}function g(o){for(;o.length;)o.pop().dispose()}export{c as CompletionAdapter,m as DefinitionAdapter,D as DiagnosticsAdapter,k as DocumentColorAdapter,R as DocumentFormattingEditProvider,p as DocumentHighlightAdapter,b as DocumentLinkAdapter,I as DocumentRangeFormattingEditProvider,_ as DocumentSymbolAdapter,v as FoldingRangeAdapter,u as HoverAdapter,f as ReferenceAdapter,w as RenameAdapter,P as SelectionRangeAdapter,A as WorkerManager,H as fromPosition,y as fromRange,F as setupMode,U as toRange,T as toTextEdit}; diff --git a/server/src/main/resources/static/assets/cypher-cTPe9QuQ.js b/server/conductor-agentspan-server/src/main/resources/static/assets/cypher-cTPe9QuQ.js similarity index 100% rename from server/src/main/resources/static/assets/cypher-cTPe9QuQ.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/cypher-cTPe9QuQ.js diff --git a/server/src/main/resources/static/assets/dart-BOtBlQCF.js b/server/conductor-agentspan-server/src/main/resources/static/assets/dart-BOtBlQCF.js similarity index 100% rename from server/src/main/resources/static/assets/dart-BOtBlQCF.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/dart-BOtBlQCF.js diff --git a/server/src/main/resources/static/assets/dockerfile-BG73LgW2.js b/server/conductor-agentspan-server/src/main/resources/static/assets/dockerfile-BG73LgW2.js similarity index 100% rename from server/src/main/resources/static/assets/dockerfile-BG73LgW2.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/dockerfile-BG73LgW2.js diff --git a/server/src/main/resources/static/assets/ecl-BEgZUVRK.js b/server/conductor-agentspan-server/src/main/resources/static/assets/ecl-BEgZUVRK.js similarity index 100% rename from server/src/main/resources/static/assets/ecl-BEgZUVRK.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/ecl-BEgZUVRK.js diff --git a/server/src/main/resources/static/assets/elixir-BkW5O-1t.js b/server/conductor-agentspan-server/src/main/resources/static/assets/elixir-BkW5O-1t.js similarity index 100% rename from server/src/main/resources/static/assets/elixir-BkW5O-1t.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/elixir-BkW5O-1t.js diff --git a/server/src/main/resources/static/assets/email-not-verified-C6p1YrlM.svg b/server/conductor-agentspan-server/src/main/resources/static/assets/email-not-verified-C6p1YrlM.svg similarity index 100% rename from server/src/main/resources/static/assets/email-not-verified-C6p1YrlM.svg rename to server/conductor-agentspan-server/src/main/resources/static/assets/email-not-verified-C6p1YrlM.svg diff --git a/server/src/main/resources/static/assets/flow9-BeJ5waoc.js b/server/conductor-agentspan-server/src/main/resources/static/assets/flow9-BeJ5waoc.js similarity index 100% rename from server/src/main/resources/static/assets/flow9-BeJ5waoc.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/flow9-BeJ5waoc.js diff --git a/server/src/main/resources/static/assets/freemarker2-DByYdBA1.js b/server/conductor-agentspan-server/src/main/resources/static/assets/freemarker2-DiIhUWTo.js similarity index 99% rename from server/src/main/resources/static/assets/freemarker2-DByYdBA1.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/freemarker2-DiIhUWTo.js index 887eaf1ff..ba8dcb044 100644 --- a/server/src/main/resources/static/assets/freemarker2-DByYdBA1.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/freemarker2-DiIhUWTo.js @@ -1,3 +1,3 @@ -import{l as c}from"./index-DiybVIaQ.js";const s=["assign","flush","ftl","return","global","import","include","break","continue","local","nested","nt","setting","stop","t","lt","rt","fallback"],d=["attempt","autoesc","autoEsc","compress","comment","escape","noescape","function","if","list","items","sep","macro","noparse","noParse","noautoesc","noAutoEsc","outputformat","switch","visit","recurse"],a={close:">",id:"angle",open:"<"},r={close:"\\]",id:"bracket",open:"\\["},F={close:"[>\\]]",id:"auto",open:"[<\\[]"},k={close:"\\}",id:"dollar",open1:"\\$",open2:"\\{"},p={close:"\\]",id:"bracket",open1:"\\[",open2:"="};function l(t){return{brackets:[["<",">"],["[","]"],["(",")"],["{","}"]],comments:{blockComment:[`${t.open}--`,`--${t.close}`]},autoCloseBefore:` +import{l as c}from"./index-DVa6sjDi.js";const s=["assign","flush","ftl","return","global","import","include","break","continue","local","nested","nt","setting","stop","t","lt","rt","fallback"],d=["attempt","autoesc","autoEsc","compress","comment","escape","noescape","function","if","list","items","sep","macro","noparse","noParse","noautoesc","noAutoEsc","outputformat","switch","visit","recurse"],a={close:">",id:"angle",open:"<"},r={close:"\\]",id:"bracket",open:"\\["},F={close:"[>\\]]",id:"auto",open:"[<\\[]"},k={close:"\\}",id:"dollar",open1:"\\$",open2:"\\{"},p={close:"\\]",id:"bracket",open1:"\\[",open2:"="};function l(t){return{brackets:[["<",">"],["[","]"],["(",")"],["{","}"]],comments:{blockComment:[`${t.open}--`,`--${t.close}`]},autoCloseBefore:` \r }]),.:;=`,autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"',notIn:["string"]},{open:"'",close:"'",notIn:["string"]}],surroundingPairs:[{open:'"',close:'"'},{open:"'",close:"'"},{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"}],folding:{markers:{start:new RegExp(`${t.open}#(?:${d.join("|")})([^/${t.close}]*(?!/)${t.close})[^${t.open}]*$`),end:new RegExp(`${t.open}/#(?:${d.join("|")})[\\r\\n\\t ]*>`)}},onEnterRules:[{beforeText:new RegExp(`${t.open}#(?!(?:${s.join("|")}))([a-zA-Z_]+)([^/${t.close}]*(?!/)${t.close})[^${t.open}]*$`),afterText:new RegExp(`^${t.open}/#([a-zA-Z_]+)[\\r\\n\\t ]*${t.close}$`),action:{indentAction:c.IndentAction.IndentOutdent}},{beforeText:new RegExp(`${t.open}#(?!(?:${s.join("|")}))([a-zA-Z_]+)([^/${t.close}]*(?!/)${t.close})[^${t.open}]*$`),action:{indentAction:c.IndentAction.Indent}}]}}function g(){return{brackets:[["<",">"],["[","]"],["(",")"],["{","}"]],autoCloseBefore:` \r }]),.:;=`,autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"',notIn:["string"]},{open:"'",close:"'",notIn:["string"]}],surroundingPairs:[{open:'"',close:'"'},{open:"'",close:"'"},{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"}],folding:{markers:{start:new RegExp(`[<\\[]#(?:${d.join("|")})([^/>\\]]*(?!/)[>\\]])[^<\\[]*$`),end:new RegExp(`[<\\[]/#(?:${d.join("|")})[\\r\\n\\t ]*>`)}},onEnterRules:[{beforeText:new RegExp(`[<\\[]#(?!(?:${s.join("|")}))([a-zA-Z_]+)([^/>\\]]*(?!/)[>\\]])[^[<\\[]]*$`),afterText:new RegExp("^[<\\[]/#([a-zA-Z_]+)[\\r\\n\\t ]*[>\\]]$"),action:{indentAction:c.IndentAction.IndentOutdent}},{beforeText:new RegExp(`[<\\[]#(?!(?:${s.join("|")}))([a-zA-Z_]+)([^/>\\]]*(?!/)[>\\]])[^[<\\[]]*$`),action:{indentAction:c.IndentAction.Indent}}]}}function _(t,n){const i=`_${t.id}_${n.id}`,e=u=>u.replace(/__id__/g,i),o=u=>{const m=u.source.replace(/__id__/g,i);return new RegExp(m,u.flags)};return{unicode:!0,includeLF:!1,start:e("default__id__"),ignoreCase:!1,defaultToken:"invalid",tokenPostfix:".freemarker2",brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],[e("open__id__")]:new RegExp(t.open),[e("close__id__")]:new RegExp(t.close),[e("iOpen1__id__")]:new RegExp(n.open1),[e("iOpen2__id__")]:new RegExp(n.open2),[e("iClose__id__")]:new RegExp(n.close),[e("startTag__id__")]:o(/(@open__id__)(#)/),[e("endTag__id__")]:o(/(@open__id__)(\/#)/),[e("startOrEndTag__id__")]:o(/(@open__id__)(\/?#)/),[e("closeTag1__id__")]:o(/((?:@blank)*)(@close__id__)/),[e("closeTag2__id__")]:o(/((?:@blank)*\/?)(@close__id__)/),blank:/[ \t\n\r]/,keywords:["false","true","in","as","using"],directiveStartCloseTag1:/attempt|recover|sep|auto[eE]sc|no(?:autoe|AutoE)sc|compress|default|no[eE]scape|comment|no[pP]arse/,directiveStartCloseTag2:/else|break|continue|return|stop|flush|t|lt|rt|nt|nested|recurse|fallback|ftl/,directiveStartBlank:/if|else[iI]f|list|for[eE]ach|switch|case|assign|global|local|include|import|function|macro|transform|visit|stop|return|call|setting|output[fF]ormat|nested|recurse|escape|ftl|items/,directiveEndCloseTag1:/if|list|items|sep|recover|attempt|for[eE]ach|local|global|assign|function|macro|output[fF]ormat|auto[eE]sc|no(?:autoe|AutoE)sc|compress|transform|switch|escape|no[eE]scape/,escapedChar:/\\(?:[ntrfbgla\\'"\{=]|(?:x[0-9A-Fa-f]{1,4}))/,asciiDigit:/[0-9]/,integer:/[0-9]+/,nonEscapedIdStartChar:/[\$@-Z_a-z\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u1FFF\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183-\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3006\u3031-\u3035\u303B-\u303C\u3040-\u318F\u31A0-\u31BA\u31F0-\u31FF\u3300-\u337F\u3400-\u4DB5\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8D0-\uA8D9\uA8F2-\uA8F7\uA8FB\uA900-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF-\uA9D9\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5-\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]/,escapedIdChar:/\\[\-\.:#]/,idStartChar:/(?:@nonEscapedIdStartChar)|(?:@escapedIdChar)/,id:/(?:@idStartChar)(?:(?:@idStartChar)|(?:@asciiDigit))*/,specialHashKeys:/\*\*|\*|false|true|in|as|using/,namedSymbols:/<=|>=|\\lte|\\lt|<|\\gte|\\gt|>|&&|\\and|->|->|==|!=|\+=|-=|\*=|\/=|%=|\+\+|--|<=|&&|\|\||:|\.\.\.|\.\.\*|\.\.<|\.\.!|\?\?|=|<|\+|-|\*|\/|%|\||\.\.|\?|!|&|\.|,|;/,arrows:["->","->"],delimiters:[";",":",",","."],stringOperators:["lte","lt","gte","gt"],noParseTags:["noparse","noParse","comment"],tokenizer:{[e("default__id__")]:[{include:e("@directive_token__id__")},{include:e("@interpolation_and_text_token__id__")}],[e("fmExpression__id__.directive")]:[{include:e("@blank_and_expression_comment_token__id__")},{include:e("@directive_end_token__id__")},{include:e("@expression_token__id__")}],[e("fmExpression__id__.interpolation")]:[{include:e("@blank_and_expression_comment_token__id__")},{include:e("@expression_token__id__")},{include:e("@greater_operators_token__id__")}],[e("inParen__id__.plain")]:[{include:e("@blank_and_expression_comment_token__id__")},{include:e("@directive_end_token__id__")},{include:e("@expression_token__id__")}],[e("inParen__id__.gt")]:[{include:e("@blank_and_expression_comment_token__id__")},{include:e("@expression_token__id__")},{include:e("@greater_operators_token__id__")}],[e("noSpaceExpression__id__")]:[{include:e("@no_space_expression_end_token__id__")},{include:e("@directive_end_token__id__")},{include:e("@expression_token__id__")}],[e("unifiedCall__id__")]:[{include:e("@unified_call_token__id__")}],[e("singleString__id__")]:[{include:e("@string_single_token__id__")}],[e("doubleString__id__")]:[{include:e("@string_double_token__id__")}],[e("rawSingleString__id__")]:[{include:e("@string_single_raw_token__id__")}],[e("rawDoubleString__id__")]:[{include:e("@string_double_raw_token__id__")}],[e("expressionComment__id__")]:[{include:e("@expression_comment_token__id__")}],[e("noParse__id__")]:[{include:e("@no_parse_token__id__")}],[e("terseComment__id__")]:[{include:e("@terse_comment_token__id__")}],[e("directive_token__id__")]:[[o(/(?:@startTag__id__)(@directiveStartCloseTag1)(?:@closeTag1__id__)/),t.id==="auto"?{cases:{"$1==<":{token:"@rematch",switchTo:`@default_angle_${n.id}`},"$1==[":{token:"@rematch",switchTo:`@default_bracket_${n.id}`}}}:[{token:"@brackets.directive"},{token:"delimiter.directive"},{cases:{"@noParseTags":{token:"tag",next:e("@noParse__id__.$3")},"@default":{token:"tag"}}},{token:"delimiter.directive"},{token:"@brackets.directive"}]],[o(/(?:@startTag__id__)(@directiveStartCloseTag2)(?:@closeTag2__id__)/),t.id==="auto"?{cases:{"$1==<":{token:"@rematch",switchTo:`@default_angle_${n.id}`},"$1==[":{token:"@rematch",switchTo:`@default_bracket_${n.id}`}}}:[{token:"@brackets.directive"},{token:"delimiter.directive"},{token:"tag"},{token:"delimiter.directive"},{token:"@brackets.directive"}]],[o(/(?:@startTag__id__)(@directiveStartBlank)(@blank)/),t.id==="auto"?{cases:{"$1==<":{token:"@rematch",switchTo:`@default_angle_${n.id}`},"$1==[":{token:"@rematch",switchTo:`@default_bracket_${n.id}`}}}:[{token:"@brackets.directive"},{token:"delimiter.directive"},{token:"tag"},{token:"",next:e("@fmExpression__id__.directive")}]],[o(/(?:@endTag__id__)(@directiveEndCloseTag1)(?:@closeTag1__id__)/),t.id==="auto"?{cases:{"$1==<":{token:"@rematch",switchTo:`@default_angle_${n.id}`},"$1==[":{token:"@rematch",switchTo:`@default_bracket_${n.id}`}}}:[{token:"@brackets.directive"},{token:"delimiter.directive"},{token:"tag"},{token:"delimiter.directive"},{token:"@brackets.directive"}]],[o(/(@open__id__)(@)/),t.id==="auto"?{cases:{"$1==<":{token:"@rematch",switchTo:`@default_angle_${n.id}`},"$1==[":{token:"@rematch",switchTo:`@default_bracket_${n.id}`}}}:[{token:"@brackets.directive"},{token:"delimiter.directive",next:e("@unifiedCall__id__")}]],[o(/(@open__id__)(\/@)((?:(?:@id)(?:\.(?:@id))*)?)(?:@closeTag1__id__)/),[{token:"@brackets.directive"},{token:"delimiter.directive"},{token:"tag"},{token:"delimiter.directive"},{token:"@brackets.directive"}]],[o(/(@open__id__)#--/),t.id==="auto"?{cases:{"$1==<":{token:"@rematch",switchTo:`@default_angle_${n.id}`},"$1==[":{token:"@rematch",switchTo:`@default_bracket_${n.id}`}}}:{token:"comment",next:e("@terseComment__id__")}],[o(/(?:@startOrEndTag__id__)([a-zA-Z_]+)/),t.id==="auto"?{cases:{"$1==<":{token:"@rematch",switchTo:`@default_angle_${n.id}`},"$1==[":{token:"@rematch",switchTo:`@default_bracket_${n.id}`}}}:[{token:"@brackets.directive"},{token:"delimiter.directive"},{token:"tag.invalid",next:e("@fmExpression__id__.directive")}]]],[e("interpolation_and_text_token__id__")]:[[o(/(@iOpen1__id__)(@iOpen2__id__)/),[{token:n.id==="bracket"?"@brackets.interpolation":"delimiter.interpolation"},{token:n.id==="bracket"?"delimiter.interpolation":"@brackets.interpolation",next:e("@fmExpression__id__.interpolation")}]],[/[\$#<\[\{]|(?:@blank)+|[^\$<#\[\{\n\r\t ]+/,{token:"source"}]],[e("string_single_token__id__")]:[[/[^'\\]/,{token:"string"}],[/@escapedChar/,{token:"string.escape"}],[/'/,{token:"string",next:"@pop"}]],[e("string_double_token__id__")]:[[/[^"\\]/,{token:"string"}],[/@escapedChar/,{token:"string.escape"}],[/"/,{token:"string",next:"@pop"}]],[e("string_single_raw_token__id__")]:[[/[^']+/,{token:"string.raw"}],[/'/,{token:"string.raw",next:"@pop"}]],[e("string_double_raw_token__id__")]:[[/[^"]+/,{token:"string.raw"}],[/"/,{token:"string.raw",next:"@pop"}]],[e("expression_token__id__")]:[[/(r?)(['"])/,{cases:{"r'":[{token:"keyword"},{token:"string.raw",next:e("@rawSingleString__id__")}],'r"':[{token:"keyword"},{token:"string.raw",next:e("@rawDoubleString__id__")}],"'":[{token:"source"},{token:"string",next:e("@singleString__id__")}],'"':[{token:"source"},{token:"string",next:e("@doubleString__id__")}]}}],[/(?:@integer)(?:\.(?:@integer))?/,{cases:{"(?:@integer)":{token:"number"},"@default":{token:"number.float"}}}],[/(\.)(@blank*)(@specialHashKeys)/,[{token:"delimiter"},{token:""},{token:"identifier"}]],[/(?:@namedSymbols)/,{cases:{"@arrows":{token:"meta.arrow"},"@delimiters":{token:"delimiter"},"@default":{token:"operators"}}}],[/@id/,{cases:{"@keywords":{token:"keyword.$0"},"@stringOperators":{token:"operators"},"@default":{token:"identifier"}}}],[/[\[\]\(\)\{\}]/,{cases:{"\\[":{cases:{"$S2==gt":{token:"@brackets",next:e("@inParen__id__.gt")},"@default":{token:"@brackets",next:e("@inParen__id__.plain")}}},"\\]":{cases:{...n.id==="bracket"?{"$S2==interpolation":{token:"@brackets.interpolation",next:"@popall"}}:{},...t.id==="bracket"?{"$S2==directive":{token:"@brackets.directive",next:"@popall"}}:{},[e("$S1==inParen__id__")]:{token:"@brackets",next:"@pop"},"@default":{token:"@brackets"}}},"\\(":{token:"@brackets",next:e("@inParen__id__.gt")},"\\)":{cases:{[e("$S1==inParen__id__")]:{token:"@brackets",next:"@pop"},"@default":{token:"@brackets"}}},"\\{":{cases:{"$S2==gt":{token:"@brackets",next:e("@inParen__id__.gt")},"@default":{token:"@brackets",next:e("@inParen__id__.plain")}}},"\\}":{cases:{...n.id==="bracket"?{}:{"$S2==interpolation":{token:"@brackets.interpolation",next:"@popall"}},[e("$S1==inParen__id__")]:{token:"@brackets",next:"@pop"},"@default":{token:"@brackets"}}}}}],[/\$\{/,{token:"delimiter.invalid"}]],[e("blank_and_expression_comment_token__id__")]:[[/(?:@blank)+/,{token:""}],[/[<\[][#!]--/,{token:"comment",next:e("@expressionComment__id__")}]],[e("directive_end_token__id__")]:[[/>/,t.id==="bracket"?{token:"operators"}:{token:"@brackets.directive",next:"@popall"}],[o(/(\/)(@close__id__)/),[{token:"delimiter.directive"},{token:"@brackets.directive",next:"@popall"}]]],[e("greater_operators_token__id__")]:[[/>/,{token:"operators"}],[/>=/,{token:"operators"}]],[e("no_space_expression_end_token__id__")]:[[/(?:@blank)+/,{token:"",switchTo:e("@fmExpression__id__.directive")}]],[e("unified_call_token__id__")]:[[/(@id)((?:@blank)+)/,[{token:"tag"},{token:"",next:e("@fmExpression__id__.directive")}]],[o(/(@id)(\/?)(@close__id__)/),[{token:"tag"},{token:"delimiter.directive"},{token:"@brackets.directive",next:"@popall"}]],[/./,{token:"@rematch",next:e("@noSpaceExpression__id__")}]],[e("no_parse_token__id__")]:[[o(/(@open__id__)(\/#?)([a-zA-Z]+)((?:@blank)*)(@close__id__)/),{cases:{"$S2==$3":[{token:"@brackets.directive"},{token:"delimiter.directive"},{token:"tag"},{token:""},{token:"@brackets.directive",next:"@popall"}],"$S2==comment":[{token:"comment"},{token:"comment"},{token:"comment"},{token:"comment"},{token:"comment"}],"@default":[{token:"source"},{token:"source"},{token:"source"},{token:"source"},{token:"source"}]}}],[/[^<\[\-]+|[<\[\-]/,{cases:{"$S2==comment":{token:"comment"},"@default":{token:"source"}}}]],[e("expression_comment_token__id__")]:[[/--[>\]]/,{token:"comment",next:"@pop"}],[/[^\->\]]+|[>\]\-]/,{token:"comment"}]],[e("terse_comment_token__id__")]:[[o(/--(?:@close__id__)/),{token:"comment",next:"@popall"}],[/[^<\[\-]+|[<\[\-]/,{token:"comment"}]]}}}function A(t){const n=_(a,t),i=_(r,t),e=_(F,t);return{...n,...i,...e,unicode:!0,includeLF:!1,start:`default_auto_${t.id}`,ignoreCase:!1,defaultToken:"invalid",tokenPostfix:".freemarker2",brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],tokenizer:{...n.tokenizer,...i.tokenizer,...e.tokenizer}}}const b={conf:l(a),language:_(a,k)},x={conf:l(r),language:_(r,k)},$={conf:l(a),language:_(a,p)},E={conf:l(r),language:_(r,p)},B={conf:g(),language:A(k)},D={conf:g(),language:A(p)};export{$ as TagAngleInterpolationBracket,b as TagAngleInterpolationDollar,D as TagAutoInterpolationBracket,B as TagAutoInterpolationDollar,E as TagBracketInterpolationBracket,x as TagBracketInterpolationDollar}; diff --git a/server/src/main/resources/static/assets/fsharp-PahG7c26.js b/server/conductor-agentspan-server/src/main/resources/static/assets/fsharp-PahG7c26.js similarity index 100% rename from server/src/main/resources/static/assets/fsharp-PahG7c26.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/fsharp-PahG7c26.js diff --git a/server/src/main/resources/static/assets/go-acbASCJo.js b/server/conductor-agentspan-server/src/main/resources/static/assets/go-acbASCJo.js similarity index 100% rename from server/src/main/resources/static/assets/go-acbASCJo.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/go-acbASCJo.js diff --git a/server/src/main/resources/static/assets/graphql-BxJiqAUM.js b/server/conductor-agentspan-server/src/main/resources/static/assets/graphql-BxJiqAUM.js similarity index 100% rename from server/src/main/resources/static/assets/graphql-BxJiqAUM.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/graphql-BxJiqAUM.js diff --git a/server/src/main/resources/static/assets/handlebars-BkkyRqxt.js b/server/conductor-agentspan-server/src/main/resources/static/assets/handlebars-jyf1sOa_.js similarity index 98% rename from server/src/main/resources/static/assets/handlebars-BkkyRqxt.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/handlebars-jyf1sOa_.js index 06eee4f50..68004b8c9 100644 --- a/server/src/main/resources/static/assets/handlebars-BkkyRqxt.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/handlebars-jyf1sOa_.js @@ -1 +1 @@ -import{l as e}from"./index-DiybVIaQ.js";const t=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],a={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:["{{!--","--}}"]},brackets:[[""],["<",">"],["{{","}}"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"<",close:">"},{open:'"',close:'"'},{open:"'",close:"'"}],onEnterRules:[{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),afterText:/^<\/(\w[\w\d]*)\s*>$/i,action:{indentAction:e.IndentAction.IndentOutdent}},{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),action:{indentAction:e.IndentAction.Indent}}]},m={defaultToken:"",tokenPostfix:"",tokenizer:{root:[[/\{\{!--/,"comment.block.start.handlebars","@commentBlock"],[/\{\{!/,"comment.start.handlebars","@comment"],[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.root"}],[/)/,["delimiter.html","tag.html","delimiter.html"]],[/(<)(script)/,["delimiter.html",{token:"tag.html",next:"@script"}]],[/(<)(style)/,["delimiter.html",{token:"tag.html",next:"@style"}]],[/(<)([:\w]+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/(<\/)(\w+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/]+/,"metatag.content.html"],[/>/,"metatag.html","@pop"]],comment:[[/\}\}/,"comment.end.handlebars","@pop"],[/./,"comment.content.handlebars"]],commentBlock:[[/--\}\}/,"comment.block.end.handlebars","@pop"],[/./,"comment.content.handlebars"]],commentHtml:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.comment"}],[/-->/,"comment.html","@pop"],[/[^-]+/,"comment.content.html"],[/./,"comment.content.html"]],otherTag:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.otherTag"}],[/\/?>/,"delimiter.html","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.script"}],[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],scriptAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterType"}],[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.scriptEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],style:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.style"}],[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],styleAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterType"}],[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.styleEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],handlebarsInSimpleState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3"}],{include:"handlebarsRoot"}],handlebarsInEmbeddedState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3",nextEmbedded:"$S3"}],{include:"handlebarsRoot"}],handlebarsRoot:[[/"[^"]*"/,"string.handlebars"],[/[#/][^\s}]+/,"keyword.helper.handlebars"],[/else\b/,"keyword.helper.handlebars"],[/[\s]+/],[/[^}]/,"variable.parameter.handlebars"]]}};export{a as conf,m as language}; +import{l as e}from"./index-DVa6sjDi.js";const t=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],a={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:["{{!--","--}}"]},brackets:[[""],["<",">"],["{{","}}"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"<",close:">"},{open:'"',close:'"'},{open:"'",close:"'"}],onEnterRules:[{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),afterText:/^<\/(\w[\w\d]*)\s*>$/i,action:{indentAction:e.IndentAction.IndentOutdent}},{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),action:{indentAction:e.IndentAction.Indent}}]},m={defaultToken:"",tokenPostfix:"",tokenizer:{root:[[/\{\{!--/,"comment.block.start.handlebars","@commentBlock"],[/\{\{!/,"comment.start.handlebars","@comment"],[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.root"}],[/)/,["delimiter.html","tag.html","delimiter.html"]],[/(<)(script)/,["delimiter.html",{token:"tag.html",next:"@script"}]],[/(<)(style)/,["delimiter.html",{token:"tag.html",next:"@style"}]],[/(<)([:\w]+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/(<\/)(\w+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/]+/,"metatag.content.html"],[/>/,"metatag.html","@pop"]],comment:[[/\}\}/,"comment.end.handlebars","@pop"],[/./,"comment.content.handlebars"]],commentBlock:[[/--\}\}/,"comment.block.end.handlebars","@pop"],[/./,"comment.content.handlebars"]],commentHtml:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.comment"}],[/-->/,"comment.html","@pop"],[/[^-]+/,"comment.content.html"],[/./,"comment.content.html"]],otherTag:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.otherTag"}],[/\/?>/,"delimiter.html","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.script"}],[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],scriptAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterType"}],[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.scriptEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],style:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.style"}],[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],styleAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterType"}],[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.styleEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],handlebarsInSimpleState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3"}],{include:"handlebarsRoot"}],handlebarsInEmbeddedState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3",nextEmbedded:"$S3"}],{include:"handlebarsRoot"}],handlebarsRoot:[[/"[^"]*"/,"string.handlebars"],[/[#/][^\s}]+/,"keyword.helper.handlebars"],[/else\b/,"keyword.helper.handlebars"],[/[\s]+/],[/[^}]/,"variable.parameter.handlebars"]]}};export{a as conf,m as language}; diff --git a/server/src/main/resources/static/assets/hcl-DtV1sZF8.js b/server/conductor-agentspan-server/src/main/resources/static/assets/hcl-DtV1sZF8.js similarity index 100% rename from server/src/main/resources/static/assets/hcl-DtV1sZF8.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/hcl-DtV1sZF8.js diff --git a/server/src/main/resources/static/assets/html-DC5114b3.js b/server/conductor-agentspan-server/src/main/resources/static/assets/html-BPD0fe3n.js similarity index 98% rename from server/src/main/resources/static/assets/html-DC5114b3.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/html-BPD0fe3n.js index 217e4c61f..e462df9bf 100644 --- a/server/src/main/resources/static/assets/html-DC5114b3.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/html-BPD0fe3n.js @@ -1 +1 @@ -import{l as e}from"./index-DiybVIaQ.js";const t=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],i={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:[""]},brackets:[[""],["<",">"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:'"',close:'"'},{open:"'",close:"'"},{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"}],onEnterRules:[{beforeText:new RegExp(`<(?!(?:${t.join("|")}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),afterText:/^<\/([_:\w][_:\w-.\d]*)\s*>$/i,action:{indentAction:e.IndentAction.IndentOutdent}},{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),action:{indentAction:e.IndentAction.Indent}}],folding:{markers:{start:new RegExp("^\\s*"),end:new RegExp("^\\s*")}}},r={defaultToken:"",tokenPostfix:".html",ignoreCase:!0,tokenizer:{root:[[/)/,["delimiter","tag","","delimiter"]],[/(<)(script)/,["delimiter",{token:"tag",next:"@script"}]],[/(<)(style)/,["delimiter",{token:"tag",next:"@style"}]],[/(<)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/(<\/)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/]+/,"metatag.content"],[/>/,"metatag","@pop"]],comment:[[/-->/,"comment","@pop"],[/[^-]+/,"comment.content"],[/./,"comment.content"]],otherTag:[[/\/?>/,"delimiter","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],scriptAfterType:[[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/"module"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.text/javascript"}],[/'module'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.text/javascript"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/>/,{token:"delimiter",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]],style:[[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],styleAfterType:[[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/>/,{token:"delimiter",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]]}};export{i as conf,r as language}; +import{l as e}from"./index-DVa6sjDi.js";const t=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],i={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:[""]},brackets:[[""],["<",">"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:'"',close:'"'},{open:"'",close:"'"},{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"}],onEnterRules:[{beforeText:new RegExp(`<(?!(?:${t.join("|")}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),afterText:/^<\/([_:\w][_:\w-.\d]*)\s*>$/i,action:{indentAction:e.IndentAction.IndentOutdent}},{beforeText:new RegExp(`<(?!(?:${t.join("|")}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,"i"),action:{indentAction:e.IndentAction.Indent}}],folding:{markers:{start:new RegExp("^\\s*"),end:new RegExp("^\\s*")}}},r={defaultToken:"",tokenPostfix:".html",ignoreCase:!0,tokenizer:{root:[[/)/,["delimiter","tag","","delimiter"]],[/(<)(script)/,["delimiter",{token:"tag",next:"@script"}]],[/(<)(style)/,["delimiter",{token:"tag",next:"@style"}]],[/(<)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/(<\/)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/]+/,"metatag.content"],[/>/,"metatag","@pop"]],comment:[[/-->/,"comment","@pop"],[/[^-]+/,"comment.content"],[/./,"comment.content"]],otherTag:[[/\/?>/,"delimiter","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],scriptAfterType:[[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/"module"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.text/javascript"}],[/'module'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.text/javascript"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/>/,{token:"delimiter",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]],style:[[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],styleAfterType:[[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/>/,{token:"delimiter",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]]}};export{i as conf,r as language}; diff --git a/server/src/main/resources/static/assets/html.worker-CwpTb9lJ.js b/server/conductor-agentspan-server/src/main/resources/static/assets/html.worker-CwpTb9lJ.js similarity index 100% rename from server/src/main/resources/static/assets/html.worker-CwpTb9lJ.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/html.worker-CwpTb9lJ.js diff --git a/server/src/main/resources/static/assets/htmlMode-B4KdkHsA.js b/server/conductor-agentspan-server/src/main/resources/static/assets/htmlMode-Dcv1flv1.js similarity index 92% rename from server/src/main/resources/static/assets/htmlMode-B4KdkHsA.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/htmlMode-Dcv1flv1.js index cc3b6d1f7..5ffb77b64 100644 --- a/server/src/main/resources/static/assets/htmlMode-B4KdkHsA.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/htmlMode-Dcv1flv1.js @@ -1 +1 @@ -import{c as D,l as t}from"./index-DiybVIaQ.js";import{H as d,D as l,h as c,F as u,b as h,S as m,c as p,f as w,g as _,C as R}from"./lspLanguageFeatures-DRGgTbma.js";import{a as E,e as H,d as b,R as y,i as T,j as U,t as x,k as M}from"./lspLanguageFeatures-DRGgTbma.js";const I=120*1e3;class f{constructor(n){this._defaults=n,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&(this._worker.dispose(),this._worker=null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){if(!this._worker)return;Date.now()-this._lastUsedTime>I&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||(this._worker=D({moduleId:"vs/language/html/htmlWorker",createWorker:()=>new Worker(new URL("/ui/assets/html.worker-CwpTb9lJ.js",import.meta.url),{type:"module"}),createData:{languageSettings:this._defaults.options,languageId:this._defaults.languageId},label:this._defaults.languageId}),this._client=this._worker.getProxy()),this._client}getLanguageServiceWorker(...n){let e;return this._getClient().then(r=>{e=r}).then(r=>{if(this._worker)return this._worker.withSyncedResources(n)}).then(r=>e)}}class v extends R{constructor(n){super(n,[".",":","<",'"',"=","/"])}}function A(i){const n=new f(i),e=(...s)=>n.getLanguageServiceWorker(...s);let r=i.languageId;t.registerCompletionItemProvider(r,new v(e)),t.registerHoverProvider(r,new d(e)),t.registerDocumentHighlightProvider(r,new l(e)),t.registerLinkProvider(r,new c(e)),t.registerFoldingRangeProvider(r,new u(e)),t.registerDocumentSymbolProvider(r,new h(e)),t.registerSelectionRangeProvider(r,new m(e)),t.registerRenameProvider(r,new p(e)),r==="html"&&(t.registerDocumentFormattingEditProvider(r,new w(e)),t.registerDocumentRangeFormattingEditProvider(r,new _(e)))}function W(i){const n=[],e=[],r=new f(i);n.push(r);const s=(...o)=>r.getLanguageServiceWorker(...o);function P(){const{languageId:o,modeConfiguration:a}=i;k(e),a.completionItems&&e.push(t.registerCompletionItemProvider(o,new v(s))),a.hovers&&e.push(t.registerHoverProvider(o,new d(s))),a.documentHighlights&&e.push(t.registerDocumentHighlightProvider(o,new l(s))),a.links&&e.push(t.registerLinkProvider(o,new c(s))),a.documentSymbols&&e.push(t.registerDocumentSymbolProvider(o,new h(s))),a.rename&&e.push(t.registerRenameProvider(o,new p(s))),a.foldingRanges&&e.push(t.registerFoldingRangeProvider(o,new u(s))),a.selectionRanges&&e.push(t.registerSelectionRangeProvider(o,new m(s))),a.documentFormattingEdits&&e.push(t.registerDocumentFormattingEditProvider(o,new w(s))),a.documentRangeFormattingEdits&&e.push(t.registerDocumentRangeFormattingEditProvider(o,new _(s)))}return P(),n.push(g(e)),g(n)}function g(i){return{dispose:()=>k(i)}}function k(i){for(;i.length;)i.pop().dispose()}export{R as CompletionAdapter,E as DefinitionAdapter,H as DiagnosticsAdapter,b as DocumentColorAdapter,w as DocumentFormattingEditProvider,l as DocumentHighlightAdapter,c as DocumentLinkAdapter,_ as DocumentRangeFormattingEditProvider,h as DocumentSymbolAdapter,u as FoldingRangeAdapter,d as HoverAdapter,y as ReferenceAdapter,p as RenameAdapter,m as SelectionRangeAdapter,f as WorkerManager,T as fromPosition,U as fromRange,W as setupMode,A as setupMode1,x as toRange,M as toTextEdit}; +import{c as D,l as t}from"./index-DVa6sjDi.js";import{H as d,D as l,h as c,F as u,b as h,S as m,c as p,f as w,g as _,C as R}from"./lspLanguageFeatures-WUkMtvXB.js";import{a as E,e as H,d as b,R as y,i as T,j as U,t as x,k as M}from"./lspLanguageFeatures-WUkMtvXB.js";const I=120*1e3;class f{constructor(n){this._defaults=n,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&(this._worker.dispose(),this._worker=null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){if(!this._worker)return;Date.now()-this._lastUsedTime>I&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||(this._worker=D({moduleId:"vs/language/html/htmlWorker",createWorker:()=>new Worker(new URL("/ui/assets/html.worker-CwpTb9lJ.js",import.meta.url),{type:"module"}),createData:{languageSettings:this._defaults.options,languageId:this._defaults.languageId},label:this._defaults.languageId}),this._client=this._worker.getProxy()),this._client}getLanguageServiceWorker(...n){let e;return this._getClient().then(r=>{e=r}).then(r=>{if(this._worker)return this._worker.withSyncedResources(n)}).then(r=>e)}}class v extends R{constructor(n){super(n,[".",":","<",'"',"=","/"])}}function A(i){const n=new f(i),e=(...s)=>n.getLanguageServiceWorker(...s);let r=i.languageId;t.registerCompletionItemProvider(r,new v(e)),t.registerHoverProvider(r,new d(e)),t.registerDocumentHighlightProvider(r,new l(e)),t.registerLinkProvider(r,new c(e)),t.registerFoldingRangeProvider(r,new u(e)),t.registerDocumentSymbolProvider(r,new h(e)),t.registerSelectionRangeProvider(r,new m(e)),t.registerRenameProvider(r,new p(e)),r==="html"&&(t.registerDocumentFormattingEditProvider(r,new w(e)),t.registerDocumentRangeFormattingEditProvider(r,new _(e)))}function W(i){const n=[],e=[],r=new f(i);n.push(r);const s=(...o)=>r.getLanguageServiceWorker(...o);function P(){const{languageId:o,modeConfiguration:a}=i;k(e),a.completionItems&&e.push(t.registerCompletionItemProvider(o,new v(s))),a.hovers&&e.push(t.registerHoverProvider(o,new d(s))),a.documentHighlights&&e.push(t.registerDocumentHighlightProvider(o,new l(s))),a.links&&e.push(t.registerLinkProvider(o,new c(s))),a.documentSymbols&&e.push(t.registerDocumentSymbolProvider(o,new h(s))),a.rename&&e.push(t.registerRenameProvider(o,new p(s))),a.foldingRanges&&e.push(t.registerFoldingRangeProvider(o,new u(s))),a.selectionRanges&&e.push(t.registerSelectionRangeProvider(o,new m(s))),a.documentFormattingEdits&&e.push(t.registerDocumentFormattingEditProvider(o,new w(s))),a.documentRangeFormattingEdits&&e.push(t.registerDocumentRangeFormattingEditProvider(o,new _(s)))}return P(),n.push(g(e)),g(n)}function g(i){return{dispose:()=>k(i)}}function k(i){for(;i.length;)i.pop().dispose()}export{R as CompletionAdapter,E as DefinitionAdapter,H as DiagnosticsAdapter,b as DocumentColorAdapter,w as DocumentFormattingEditProvider,l as DocumentHighlightAdapter,c as DocumentLinkAdapter,_ as DocumentRangeFormattingEditProvider,h as DocumentSymbolAdapter,u as FoldingRangeAdapter,d as HoverAdapter,y as ReferenceAdapter,p as RenameAdapter,m as SelectionRangeAdapter,f as WorkerManager,T as fromPosition,U as fromRange,W as setupMode,A as setupMode1,x as toRange,M as toTextEdit}; diff --git a/server/src/main/resources/static/assets/index-DNgKRNTO.css b/server/conductor-agentspan-server/src/main/resources/static/assets/index-DNgKRNTO.css similarity index 100% rename from server/src/main/resources/static/assets/index-DNgKRNTO.css rename to server/conductor-agentspan-server/src/main/resources/static/assets/index-DNgKRNTO.css diff --git a/server/src/main/resources/static/assets/index-DiybVIaQ.js b/server/conductor-agentspan-server/src/main/resources/static/assets/index-DVa6sjDi.js similarity index 70% rename from server/src/main/resources/static/assets/index-DiybVIaQ.js rename to server/conductor-agentspan-server/src/main/resources/static/assets/index-DVa6sjDi.js index f271763e2..aed981c48 100644 --- a/server/src/main/resources/static/assets/index-DiybVIaQ.js +++ b/server/conductor-agentspan-server/src/main/resources/static/assets/index-DVa6sjDi.js @@ -1,6 +1,6 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/cssMode-DczLDKZ6.js","assets/lspLanguageFeatures-DRGgTbma.js","assets/htmlMode-B4KdkHsA.js","assets/jsonMode-BbtaPfU1.js","assets/javascript-PBX7W67C.js","assets/typescript-n8wEprT6.js"])))=>i.map(i=>d[i]); -function Ulr(n,e){for(var t=0;ti[r]})}}}return Object.freeze(Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))i(r);new MutationObserver(r=>{for(const o of r)if(o.type==="childList")for(const l of o.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&i(l)}).observe(document,{childList:!0,subtree:!0});function t(r){const o={};return r.integrity&&(o.integrity=r.integrity),r.referrerPolicy&&(o.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?o.credentials="include":r.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function i(r){if(r.ep)return;r.ep=!0;const o=t(r);fetch(r.href,o)}})();var Cy=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Vs(n){return n&&n.__esModule&&Object.prototype.hasOwnProperty.call(n,"default")?n.default:n}function o5e(n){if(Object.prototype.hasOwnProperty.call(n,"__esModule"))return n;var e=n.default;if(typeof e=="function"){var t=function i(){var r=!1;try{r=this instanceof i}catch{}return r?Reflect.construct(e,arguments,this.constructor):e.apply(this,arguments)};t.prototype=e.prototype}else t={};return Object.defineProperty(t,"__esModule",{value:!0}),Object.keys(n).forEach(function(i){var r=Object.getOwnPropertyDescriptor(n,i);Object.defineProperty(t,i,r.get?r:{enumerable:!0,get:function(){return n[i]}})}),t}var gEt={exports:{}},Mxe={},mEt={exports:{}},wd={};var aNn;function qlr(){if(aNn)return wd;aNn=1;var n=Symbol.for("react.element"),e=Symbol.for("react.portal"),t=Symbol.for("react.fragment"),i=Symbol.for("react.strict_mode"),r=Symbol.for("react.profiler"),o=Symbol.for("react.provider"),l=Symbol.for("react.context"),c=Symbol.for("react.forward_ref"),d=Symbol.for("react.suspense"),h=Symbol.for("react.memo"),p=Symbol.for("react.lazy"),m=Symbol.iterator;function b(ce){return ce===null||typeof ce!="object"?null:(ce=m&&ce[m]||ce["@@iterator"],typeof ce=="function"?ce:null)}var w={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_=Object.assign,x={};function T(ce,ye,he){this.props=ce,this.context=ye,this.refs=x,this.updater=he||w}T.prototype.isReactComponent={},T.prototype.setState=function(ce,ye){if(typeof ce!="object"&&typeof ce!="function"&&ce!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,ce,ye,"setState")},T.prototype.forceUpdate=function(ce){this.updater.enqueueForceUpdate(this,ce,"forceUpdate")};function I(){}I.prototype=T.prototype;function L(ce,ye,he){this.props=ce,this.context=ye,this.refs=x,this.updater=he||w}var A=L.prototype=new I;A.constructor=L,_(A,T.prototype),A.isPureReactComponent=!0;var M=Array.isArray,O=Object.prototype.hasOwnProperty,F={current:null},j={key:!0,ref:!0,__self:!0,__source:!0};function W(ce,ye,he){var pe,me={},be=null,xe=null;if(ye!=null)for(pe in ye.ref!==void 0&&(xe=ye.ref),ye.key!==void 0&&(be=""+ye.key),ye)O.call(ye,pe)&&!j.hasOwnProperty(pe)&&(me[pe]=ye[pe]);var Te=arguments.length-2;if(Te===1)me.children=he;else if(1t.searchParams.append("args[]",i)),`Minified MUI error #${n}; visit ${t} for the full message.`}function ii(n){if(typeof n!="string")throw new Error(bW(7));return n.charAt(0).toUpperCase()+n.slice(1)}var yEt={exports:{}},yg={};var pNn;function Qlr(){if(pNn)return yg;pNn=1;var n=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),t=Symbol.for("react.fragment"),i=Symbol.for("react.strict_mode"),r=Symbol.for("react.profiler"),o=Symbol.for("react.consumer"),l=Symbol.for("react.context"),c=Symbol.for("react.forward_ref"),d=Symbol.for("react.suspense"),h=Symbol.for("react.suspense_list"),p=Symbol.for("react.memo"),m=Symbol.for("react.lazy"),b=Symbol.for("react.view_transition"),w=Symbol.for("react.client.reference");function _(x){if(typeof x=="object"&&x!==null){var T=x.$$typeof;switch(T){case n:switch(x=x.type,x){case t:case r:case i:case d:case h:case b:return x;default:switch(x=x&&x.$$typeof,x){case l:case c:case m:case p:return x;case o:return x;default:return T}}case e:return T}}}return yg.ContextConsumer=o,yg.ContextProvider=l,yg.Element=n,yg.ForwardRef=c,yg.Fragment=t,yg.Lazy=m,yg.Memo=p,yg.Portal=e,yg.Profiler=r,yg.StrictMode=i,yg.Suspense=d,yg.SuspenseList=h,yg.isContextConsumer=function(x){return _(x)===o},yg.isContextProvider=function(x){return _(x)===l},yg.isElement=function(x){return typeof x=="object"&&x!==null&&x.$$typeof===n},yg.isForwardRef=function(x){return _(x)===c},yg.isFragment=function(x){return _(x)===t},yg.isLazy=function(x){return _(x)===m},yg.isMemo=function(x){return _(x)===p},yg.isPortal=function(x){return _(x)===e},yg.isProfiler=function(x){return _(x)===r},yg.isStrictMode=function(x){return _(x)===i},yg.isSuspense=function(x){return _(x)===d},yg.isSuspenseList=function(x){return _(x)===h},yg.isValidElementType=function(x){return typeof x=="string"||typeof x=="function"||x===t||x===r||x===i||x===d||x===h||typeof x=="object"&&x!==null&&(x.$$typeof===m||x.$$typeof===p||x.$$typeof===l||x.$$typeof===o||x.$$typeof===c||x.$$typeof===w||x.getModuleId!==void 0)},yg.typeOf=_,yg}var gNn;function Jlr(){return gNn||(gNn=1,yEt.exports=Qlr()),yEt.exports}var wet=Jlr();function KP(n){if(typeof n!="object"||n===null)return!1;const e=Object.getPrototypeOf(n);return(e===null||e===Object.prototype||Object.getPrototypeOf(e)===null)&&!(Symbol.toStringTag in n)&&!(Symbol.iterator in n)}function Bli(n){if(D.isValidElement(n)||wet.isValidElementType(n)||!KP(n))return n;const e={};return Object.keys(n).forEach(t=>{e[t]=Bli(n[t])}),e}function O_(n,e,t={clone:!0}){const i=t.clone?{...n}:n;return KP(n)&&KP(e)&&Object.keys(e).forEach(r=>{D.isValidElement(e[r])||wet.isValidElementType(e[r])?i[r]=e[r]:KP(e[r])&&Object.prototype.hasOwnProperty.call(n,r)&&KP(n[r])?i[r]=O_(n[r],e[r],t):t.clone?i[r]=KP(e[r])?Bli(e[r]):e[r]:i[r]=e[r]}),i}function t4e(n,e){return e?O_(n,e,{clone:!1}):n}function mNn(n,e){if(!n.containerQueries)return e;const t=Object.keys(e).filter(i=>i.startsWith("@container")).sort((i,r)=>{const o=/min-width:\s*([0-9.]+)/;return+(i.match(o)?.[1]||0)-+(r.match(o)?.[1]||0)});return t.length?t.reduce((i,r)=>{const o=e[r];return delete i[r],i[r]=o,i},{...e}):e}function ecr(n,e){return e==="@"||e.startsWith("@")&&(n.some(t=>e.startsWith(`@${t}`))||!!e.match(/^@\d/))}function tcr(n,e){const t=e.match(/^@([^/]+)?\/?(.+)?$/);if(!t)return null;const[,i,r]=t,o=Number.isNaN(+i)?i||0:+i;return n.containerQueries(r).up(o)}function ncr(n){const e=(o,l)=>o.replace("@media",l?`@container ${l}`:"@container");function t(o,l){o.up=(...c)=>e(n.breakpoints.up(...c),l),o.down=(...c)=>e(n.breakpoints.down(...c),l),o.between=(...c)=>e(n.breakpoints.between(...c),l),o.only=(...c)=>e(n.breakpoints.only(...c),l),o.not=(...c)=>{const d=e(n.breakpoints.not(...c),l);return d.includes("not all and")?d.replace("not all and ","").replace("min-width:","width<").replace("max-width:","width>").replace("and","or"):d}}const i={},r=o=>(t(i,o),i);return t(r),{...n,containerQueries:r}}const yet={xs:0,sm:600,md:900,lg:1200,xl:1536},bNn={keys:["xs","sm","md","lg","xl"],up:n=>`@media (min-width:${yet[n]}px)`},icr={containerQueries:n=>({up:e=>{let t=typeof e=="number"?e:yet[e]||e;return typeof t=="number"&&(t=`${t}px`),n?`@container ${n} (min-width:${t})`:`@container (min-width:${t})`}})};function dM(n,e,t){const i=n.theme||{};if(Array.isArray(e)){const o=i.breakpoints||bNn;return e.reduce((l,c,d)=>(l[o.up(o.keys[d])]=t(e[d]),l),{})}if(typeof e=="object"){const o=i.breakpoints||bNn;return Object.keys(e).reduce((l,c)=>{if(ecr(o.keys,c)){const d=tcr(i.containerQueries?i:icr,c);d&&(l[d]=t(e[c],c))}else if(Object.keys(o.values||yet).includes(c)){const d=o.up(c);l[d]=t(e[c],c)}else{const d=c;l[d]=e[d]}return l},{})}return t(e)}function Wli(n={}){return n.keys?.reduce((t,i)=>{const r=n.up(i);return t[r]={},t},{})||{}}function SOt(n,e){return n.reduce((t,i)=>{const r=t[i];return(!r||Object.keys(r).length===0)&&delete t[i],t},e)}function rcr(n,...e){const t=Wli(n),i=[t,...e].reduce((r,o)=>O_(r,o),{});return SOt(Object.keys(t),i)}function scr(n,e){if(typeof n!="object")return{};const t={},i=Object.keys(e);return Array.isArray(n)?i.forEach((r,o)=>{o{n[r]!=null&&(t[r]=!0)}),t}function _Et({values:n,breakpoints:e,base:t}){const i=t||scr(n,e),r=Object.keys(i);if(r.length===0)return n;let o;return r.reduce((l,c,d)=>(Array.isArray(n)?(l[c]=n[d]!=null?n[d]:n[o],o=d):typeof n=="object"?(l[c]=n[c]!=null?n[c]:n[o],o=c):l[c]=n,l),{})}function jP(n,e,t=!0){if(!e||typeof e!="string")return null;if(n&&n.vars&&t){const i=`vars.${e}`.split(".").reduce((r,o)=>r&&r[o]?r[o]:null,n);if(i!=null)return i}return e.split(".").reduce((i,r)=>i&&i[r]!=null?i[r]:null,n)}function Gqe(n,e,t,i=t){let r;return typeof n=="function"?r=n(t):Array.isArray(n)?r=n[t]||i:r=jP(n,t)||i,e&&(r=e(r,i,n)),r}function wv(n){const{prop:e,cssProperty:t=n.prop,themeKey:i,transform:r}=n,o=l=>{if(l[e]==null)return null;const c=l[e],d=l.theme,h=jP(d,i)||{};return dM(l,c,m=>{let b=Gqe(h,r,m);return m===b&&typeof m=="string"&&(b=Gqe(h,r,`${e}${m==="default"?"":ii(m)}`,m)),t===!1?b:{[t]:b}})};return o.propTypes={},o.filterProps=[e],o}function ocr(n){const e={};return t=>(e[t]===void 0&&(e[t]=n(t)),e[t])}const acr={m:"margin",p:"padding"},lcr={t:"Top",r:"Right",b:"Bottom",l:"Left",x:["Left","Right"],y:["Top","Bottom"]},vNn={marginX:"mx",marginY:"my",paddingX:"px",paddingY:"py"},ccr=ocr(n=>{if(n.length>2)if(vNn[n])n=vNn[n];else return[n];const[e,t]=n.split(""),i=acr[e],r=lcr[t]||"";return Array.isArray(r)?r.map(o=>i+o):[i+r]}),yzt=["m","mt","mr","mb","ml","mx","my","margin","marginTop","marginRight","marginBottom","marginLeft","marginX","marginY","marginInline","marginInlineStart","marginInlineEnd","marginBlock","marginBlockStart","marginBlockEnd"],_zt=["p","pt","pr","pb","pl","px","py","padding","paddingTop","paddingRight","paddingBottom","paddingLeft","paddingX","paddingY","paddingInline","paddingInlineStart","paddingInlineEnd","paddingBlock","paddingBlockStart","paddingBlockEnd"];[...yzt,..._zt];function a5e(n,e,t,i){const r=jP(n,e,!0)??t;return typeof r=="number"||typeof r=="string"?o=>typeof o=="string"?o:typeof r=="string"?r.startsWith("var(")&&o===0?0:r.startsWith("var(")&&o===1?r:`calc(${o} * ${r})`:r*o:Array.isArray(r)?o=>{if(typeof o=="string")return o;const l=Math.abs(o),c=r[l];return o>=0?c:typeof c=="number"?-c:typeof c=="string"&&c.startsWith("var(")?`calc(-1 * ${c})`:`-${c}`}:typeof r=="function"?r:()=>{}}function _et(n){return a5e(n,"spacing",8)}function Qre(n,e){return typeof e=="string"||e==null?e:n(e)}function ucr(n,e){return t=>n.reduce((i,r)=>(i[r]=Qre(e,t),i),{})}function dcr(n,e,t,i){if(!e.includes(t))return null;const r=ccr(t),o=ucr(r,i),l=n[t];return dM(n,l,o)}function Vli(n,e){const t=_et(n.theme);return Object.keys(n).map(i=>dcr(n,e,i,t)).reduce(t4e,{})}function tb(n){return Vli(n,yzt)}tb.propTypes={};tb.filterProps=yzt;function nb(n){return Vli(n,_zt)}nb.propTypes={};nb.filterProps=_zt;function Cet(...n){const e=n.reduce((i,r)=>(r.filterProps.forEach(o=>{i[o]=r}),i),{}),t=i=>Object.keys(i).reduce((r,o)=>e[o]?t4e(r,e[o](i)):r,{});return t.propTypes={},t.filterProps=n.reduce((i,r)=>i.concat(r.filterProps),[]),t}function i3(n){return typeof n!="number"?n:`${n}px solid`}function B3(n,e){return wv({prop:n,themeKey:"borders",transform:e})}const hcr=B3("border",i3),fcr=B3("borderTop",i3),pcr=B3("borderRight",i3),gcr=B3("borderBottom",i3),mcr=B3("borderLeft",i3),bcr=B3("borderColor"),vcr=B3("borderTopColor"),wcr=B3("borderRightColor"),ycr=B3("borderBottomColor"),_cr=B3("borderLeftColor"),Ccr=B3("outline",i3),Scr=B3("outlineColor"),xet=n=>{if(n.borderRadius!==void 0&&n.borderRadius!==null){const e=a5e(n.theme,"shape.borderRadius",4),t=i=>({borderRadius:Qre(e,i)});return dM(n,n.borderRadius,t)}return null};xet.propTypes={};xet.filterProps=["borderRadius"];Cet(hcr,fcr,pcr,gcr,mcr,bcr,vcr,wcr,ycr,_cr,xet,Ccr,Scr);const Eet=n=>{if(n.gap!==void 0&&n.gap!==null){const e=a5e(n.theme,"spacing",8),t=i=>({gap:Qre(e,i)});return dM(n,n.gap,t)}return null};Eet.propTypes={};Eet.filterProps=["gap"];const ket=n=>{if(n.columnGap!==void 0&&n.columnGap!==null){const e=a5e(n.theme,"spacing",8),t=i=>({columnGap:Qre(e,i)});return dM(n,n.columnGap,t)}return null};ket.propTypes={};ket.filterProps=["columnGap"];const Tet=n=>{if(n.rowGap!==void 0&&n.rowGap!==null){const e=a5e(n.theme,"spacing",8),t=i=>({rowGap:Qre(e,i)});return dM(n,n.rowGap,t)}return null};Tet.propTypes={};Tet.filterProps=["rowGap"];const xcr=wv({prop:"gridColumn"}),Ecr=wv({prop:"gridRow"}),kcr=wv({prop:"gridAutoFlow"}),Tcr=wv({prop:"gridAutoColumns"}),Lcr=wv({prop:"gridAutoRows"}),Dcr=wv({prop:"gridTemplateColumns"}),Icr=wv({prop:"gridTemplateRows"}),Acr=wv({prop:"gridTemplateAreas"}),Rcr=wv({prop:"gridArea"});Cet(Eet,ket,Tet,xcr,Ecr,kcr,Tcr,Lcr,Dcr,Icr,Acr,Rcr);function Ype(n,e){return e==="grey"?e:n}const Mcr=wv({prop:"color",themeKey:"palette",transform:Ype}),Ocr=wv({prop:"bgcolor",cssProperty:"backgroundColor",themeKey:"palette",transform:Ype}),Ncr=wv({prop:"backgroundColor",themeKey:"palette",transform:Ype});Cet(Mcr,Ocr,Ncr);function D6(n){return n<=1&&n!==0?`${n*100}%`:n}const Pcr=wv({prop:"width",transform:D6}),Czt=n=>{if(n.maxWidth!==void 0&&n.maxWidth!==null){const e=t=>{const i=n.theme?.breakpoints?.values?.[t]||yet[t];return i?n.theme?.breakpoints?.unit!=="px"?{maxWidth:`${i}${n.theme.breakpoints.unit}`}:{maxWidth:i}:{maxWidth:D6(t)}};return dM(n,n.maxWidth,e)}return null};Czt.filterProps=["maxWidth"];const Fcr=wv({prop:"minWidth",transform:D6}),jcr=wv({prop:"height",transform:D6}),Hcr=wv({prop:"maxHeight",transform:D6}),Bcr=wv({prop:"minHeight",transform:D6});wv({prop:"size",cssProperty:"width",transform:D6});wv({prop:"size",cssProperty:"height",transform:D6});const Wcr=wv({prop:"boxSizing"});Cet(Pcr,Czt,Fcr,jcr,Hcr,Bcr,Wcr);const l5e={border:{themeKey:"borders",transform:i3},borderTop:{themeKey:"borders",transform:i3},borderRight:{themeKey:"borders",transform:i3},borderBottom:{themeKey:"borders",transform:i3},borderLeft:{themeKey:"borders",transform:i3},borderColor:{themeKey:"palette"},borderTopColor:{themeKey:"palette"},borderRightColor:{themeKey:"palette"},borderBottomColor:{themeKey:"palette"},borderLeftColor:{themeKey:"palette"},outline:{themeKey:"borders",transform:i3},outlineColor:{themeKey:"palette"},borderRadius:{themeKey:"shape.borderRadius",style:xet},color:{themeKey:"palette",transform:Ype},bgcolor:{themeKey:"palette",cssProperty:"backgroundColor",transform:Ype},backgroundColor:{themeKey:"palette",transform:Ype},p:{style:nb},pt:{style:nb},pr:{style:nb},pb:{style:nb},pl:{style:nb},px:{style:nb},py:{style:nb},padding:{style:nb},paddingTop:{style:nb},paddingRight:{style:nb},paddingBottom:{style:nb},paddingLeft:{style:nb},paddingX:{style:nb},paddingY:{style:nb},paddingInline:{style:nb},paddingInlineStart:{style:nb},paddingInlineEnd:{style:nb},paddingBlock:{style:nb},paddingBlockStart:{style:nb},paddingBlockEnd:{style:nb},m:{style:tb},mt:{style:tb},mr:{style:tb},mb:{style:tb},ml:{style:tb},mx:{style:tb},my:{style:tb},margin:{style:tb},marginTop:{style:tb},marginRight:{style:tb},marginBottom:{style:tb},marginLeft:{style:tb},marginX:{style:tb},marginY:{style:tb},marginInline:{style:tb},marginInlineStart:{style:tb},marginInlineEnd:{style:tb},marginBlock:{style:tb},marginBlockStart:{style:tb},marginBlockEnd:{style:tb},displayPrint:{cssProperty:!1,transform:n=>({"@media print":{display:n}})},display:{},overflow:{},textOverflow:{},visibility:{},whiteSpace:{},flexBasis:{},flexDirection:{},flexWrap:{},justifyContent:{},alignItems:{},alignContent:{},order:{},flex:{},flexGrow:{},flexShrink:{},alignSelf:{},justifyItems:{},justifySelf:{},gap:{style:Eet},rowGap:{style:Tet},columnGap:{style:ket},gridColumn:{},gridRow:{},gridAutoFlow:{},gridAutoColumns:{},gridAutoRows:{},gridTemplateColumns:{},gridTemplateRows:{},gridTemplateAreas:{},gridArea:{},position:{},zIndex:{themeKey:"zIndex"},top:{},right:{},bottom:{},left:{},boxShadow:{themeKey:"shadows"},width:{transform:D6},maxWidth:{style:Czt},minWidth:{transform:D6},height:{transform:D6},maxHeight:{transform:D6},minHeight:{transform:D6},boxSizing:{},font:{themeKey:"font"},fontFamily:{themeKey:"typography"},fontSize:{themeKey:"typography"},fontStyle:{themeKey:"typography"},fontWeight:{themeKey:"typography"},letterSpacing:{},textTransform:{},lineHeight:{},textAlign:{},typography:{cssProperty:!1,themeKey:"typography"}};function Vcr(...n){const e=n.reduce((i,r)=>i.concat(Object.keys(r)),[]),t=new Set(e);return n.every(i=>t.size===Object.keys(i).length)}function $cr(n,e){return typeof n=="function"?n(e):n}function zcr(){function n(t,i,r,o){const l={[t]:i,theme:r},c=o[t];if(!c)return{[t]:i};const{cssProperty:d=t,themeKey:h,transform:p,style:m}=c;if(i==null)return null;if(h==="typography"&&i==="inherit")return{[t]:i};const b=jP(r,h)||{};return m?m(l):dM(l,i,_=>{let x=Gqe(b,p,_);return _===x&&typeof _=="string"&&(x=Gqe(b,p,`${t}${_==="default"?"":ii(_)}`,_)),d===!1?x:{[d]:x}})}function e(t){const{sx:i,theme:r={},nested:o}=t||{};if(!i)return null;const l=r.unstable_sxConfig??l5e;function c(d){let h=d;if(typeof d=="function")h=d(r);else if(typeof d!="object")return d;if(!h)return null;const p=Wli(r.breakpoints),m=Object.keys(p);let b=p;return Object.keys(h).forEach(w=>{const _=$cr(h[w],r);if(_!=null)if(typeof _=="object")if(l[w])b=t4e(b,n(w,_,r,l));else{const x=dM({theme:r},_,T=>({[w]:T}));Vcr(x,_)?b[w]=e({sx:_,theme:r,nested:!0}):b=t4e(b,x)}else b=t4e(b,n(w,_,r,l))}),!o&&r.modularCssLayers?{"@layer sx":mNn(r,SOt(m,b))}:mNn(r,SOt(m,b))}return Array.isArray(i)?i.map(c):c(i)}return e}const ZK=zcr();ZK.filterProps=["sx"];const Ucr=n=>{const e={systemProps:{},otherProps:{}},t=n?.theme?.unstable_sxConfig??l5e;return Object.keys(n).forEach(i=>{t[i]?e.systemProps[i]=n[i]:e.otherProps[i]=n[i]}),e};function Let(n){const{sx:e,...t}=n,{systemProps:i,otherProps:r}=Ucr(t);let o;return Array.isArray(e)?o=[i,...e]:typeof e=="function"?o=(...l)=>{const c=e(...l);return KP(c)?{...i,...c}:i}:o={...i,...e},{...r,sx:o}}function Zt(){return Zt=Object.assign?Object.assign.bind():function(n){for(var e=1;e0?U2(z1e,--sT):0,hme--,rw===10&&(hme=1,Iet--),rw}function B6(){return rw=sT2||sLe(rw)>3?"":" "}function sur(n,e){for(;--e&&B6()&&!(rw<48||rw>102||rw>57&&rw<65||rw>70&&rw<97););return c5e(n,dUe()+(e<6&&C9()==32&&B6()==32))}function EOt(n){for(;B6();)switch(rw){case n:return sT;case 34:case 39:n!==34&&n!==39&&EOt(rw);break;case 40:n===41&&EOt(n);break;case 92:B6();break}return sT}function our(n,e){for(;B6()&&n+rw!==57;)if(n+rw===84&&C9()===47)break;return"/*"+c5e(e,sT-1)+"*"+Det(n===47?n:B6())}function aur(n){for(;!sLe(C9());)B6();return c5e(n,sT)}function lur(n){return Kli(fUe("",null,null,null,[""],n=Gli(n),0,[0],n))}function fUe(n,e,t,i,r,o,l,c,d){for(var h=0,p=0,m=l,b=0,w=0,_=0,x=1,T=1,I=1,L=0,A="",M=r,O=o,F=i,j=A;T;)switch(_=L,L=B6()){case 40:if(_!=108&&U2(j,m-1)==58){xOt(j+=Uf(hUe(L),"&","&\f"),"&\f")!=-1&&(I=-1);break}case 34:case 39:case 91:j+=hUe(L);break;case 9:case 10:case 13:case 32:j+=rur(_);break;case 92:j+=sur(dUe()-1,7);continue;case 47:switch(C9()){case 42:case 47:SBe(cur(our(B6(),dUe()),e,t),d);break;default:j+="/"}break;case 123*x:c[h++]=HP(j)*I;case 125*x:case 59:case 0:switch(L){case 0:case 125:T=0;case 59+p:I==-1&&(j=Uf(j,/\f/g,"")),w>0&&HP(j)-m&&SBe(w>32?yNn(j+";",i,t,m-1):yNn(Uf(j," ","")+";",i,t,m-2),d);break;case 59:j+=";";default:if(SBe(F=wNn(j,e,t,h,p,r,c,A,M=[],O=[],m),o),L===123)if(p===0)fUe(j,e,F,F,M,o,m,c,O);else switch(b===99&&U2(j,3)===110?100:b){case 100:case 108:case 109:case 115:fUe(n,F,F,i&&SBe(wNn(n,F,F,0,0,r,c,A,r,M=[],m),O),r,O,m,c,i?M:O);break;default:fUe(j,F,F,F,[""],O,0,c,O)}}h=p=w=0,x=I=1,A=j="",m=l;break;case 58:m=1+HP(j),w=_;default:if(x<1){if(L==123)--x;else if(L==125&&x++==0&&iur()==125)continue}switch(j+=Det(L),L*x){case 38:I=p>0?1:(j+="\f",-1);break;case 44:c[h++]=(HP(j)-1)*I,I=1;break;case 64:C9()===45&&(j+=hUe(B6())),b=C9(),p=m=HP(A=j+=aur(dUe())),L++;break;case 45:_===45&&HP(j)==2&&(x=0)}}return o}function wNn(n,e,t,i,r,o,l,c,d,h,p){for(var m=r-1,b=r===0?o:[""],w=Ezt(b),_=0,x=0,T=0;_0?b[I]+" "+L:Uf(L,/&\f/g,b[I])))&&(d[T++]=A);return Aet(n,e,t,r===0?Szt:c,d,h,p)}function cur(n,e,t){return Aet(n,e,t,$li,Det(nur()),rLe(n,2,-2),0)}function yNn(n,e,t,i){return Aet(n,e,t,xzt,rLe(n,0,i),rLe(n,i+1,-1),i)}function Zpe(n,e){for(var t="",i=Ezt(n),r=0;r6)switch(U2(n,e+1)){case 109:if(U2(n,e+4)!==45)break;case 102:return Uf(n,/(.+:)(.+)-([^]+)/,"$1"+$f+"$2-$3$1"+Kqe+(U2(n,e+3)==108?"$3":"$2-$3"))+n;case 115:return~xOt(n,"stretch")?Zli(Uf(n,"stretch","fill-available"),e)+n:n}break;case 4949:if(U2(n,e+1)!==115)break;case 6444:switch(U2(n,HP(n)-3-(~xOt(n,"!important")&&10))){case 107:return Uf(n,":",":"+$f)+n;case 101:return Uf(n,/(.+:)([^;!]+)(;|!.+)?/,"$1"+$f+(U2(n,14)===45?"inline-":"")+"box$3$1"+$f+"$2$3$1"+cS+"$2box$3")+n}break;case 5936:switch(U2(n,e+11)){case 114:return $f+n+cS+Uf(n,/[svh]\w+-[tblr]{2}/,"tb")+n;case 108:return $f+n+cS+Uf(n,/[svh]\w+-[tblr]{2}/,"tb-rl")+n;case 45:return $f+n+cS+Uf(n,/[svh]\w+-[tblr]{2}/,"lr")+n}return $f+n+cS+n+n}return n}var vur=function(e,t,i,r){if(e.length>-1&&!e.return)switch(e.type){case xzt:e.return=Zli(e.value,e.length);break;case zli:return Zpe([Oxe(e,{value:Uf(e.value,"@","@"+$f)})],r);case Szt:if(e.length)return tur(e.props,function(o){switch(eur(o,/(::plac\w+|:read-\w+)/)){case":read-only":case":read-write":return Zpe([Oxe(e,{props:[Uf(o,/:(read-\w+)/,":"+Kqe+"$1")]})],r);case"::placeholder":return Zpe([Oxe(e,{props:[Uf(o,/:(plac\w+)/,":"+$f+"input-$1")]}),Oxe(e,{props:[Uf(o,/:(plac\w+)/,":"+Kqe+"$1")]}),Oxe(e,{props:[Uf(o,/:(plac\w+)/,cS+"input-$1")]})],r)}return""})}},wur=[vur],yur=function(e){var t=e.key;if(t==="css"){var i=document.querySelectorAll("style[data-emotion]:not([data-s])");Array.prototype.forEach.call(i,function(x){var T=x.getAttribute("data-emotion");T.indexOf(" ")!==-1&&(document.head.appendChild(x),x.setAttribute("data-s",""))})}var r=e.stylisPlugins||wur,o={},l,c=[];l=e.container||document.head,Array.prototype.forEach.call(document.querySelectorAll('style[data-emotion^="'+t+' "]'),function(x){for(var T=x.getAttribute("data-emotion").split(" "),I=1;I=4;++i,r-=4)t=n.charCodeAt(i)&255|(n.charCodeAt(++i)&255)<<8|(n.charCodeAt(++i)&255)<<16|(n.charCodeAt(++i)&255)<<24,t=(t&65535)*1540483477+((t>>>16)*59797<<16),t^=t>>>24,e=(t&65535)*1540483477+((t>>>16)*59797<<16)^(e&65535)*1540483477+((e>>>16)*59797<<16);switch(r){case 3:e^=(n.charCodeAt(i+2)&255)<<16;case 2:e^=(n.charCodeAt(i+1)&255)<<8;case 1:e^=n.charCodeAt(i)&255,e=(e&65535)*1540483477+((e>>>16)*59797<<16)}return e^=e>>>13,e=(e&65535)*1540483477+((e>>>16)*59797<<16),((e^e>>>15)>>>0).toString(36)}var Lur={animationIterationCount:1,aspectRatio:1,borderImageOutset:1,borderImageSlice:1,borderImageWidth:1,boxFlex:1,boxFlexGroup:1,boxOrdinalGroup:1,columnCount:1,columns:1,flex:1,flexGrow:1,flexPositive:1,flexShrink:1,flexNegative:1,flexOrder:1,gridRow:1,gridRowEnd:1,gridRowSpan:1,gridRowStart:1,gridColumn:1,gridColumnEnd:1,gridColumnSpan:1,gridColumnStart:1,msGridRow:1,msGridRowSpan:1,msGridColumn:1,msGridColumnSpan:1,fontWeight:1,lineHeight:1,opacity:1,order:1,orphans:1,scale:1,tabSize:1,widows:1,zIndex:1,zoom:1,WebkitLineClamp:1,fillOpacity:1,floodOpacity:1,stopOpacity:1,strokeDasharray:1,strokeDashoffset:1,strokeMiterlimit:1,strokeOpacity:1,strokeWidth:1},Dur=/[A-Z]|^ms/g,Iur=/_EMO_([^_]+?)_([^]*?)_EMO_/g,Qli=function(e){return e.charCodeAt(1)===45},ENn=function(e){return e!=null&&typeof e!="boolean"},xEt=Yli(function(n){return Qli(n)?n:n.replace(Dur,"-$&").toLowerCase()}),kNn=function(e,t){switch(e){case"animation":case"animationName":if(typeof t=="string")return t.replace(Iur,function(i,r,o){return BP={name:r,styles:o,next:BP},r})}return Lur[e]!==1&&!Qli(e)&&typeof t=="number"&&t!==0?t+"px":t};function oLe(n,e,t){if(t==null)return"";var i=t;if(i.__emotion_styles!==void 0)return i;switch(typeof t){case"boolean":return"";case"object":{var r=t;if(r.anim===1)return BP={name:r.name,styles:r.styles,next:BP},r.name;var o=t;if(o.styles!==void 0){var l=o.next;if(l!==void 0)for(;l!==void 0;)BP={name:l.name,styles:l.styles,next:BP},l=l.next;var c=o.styles+";";return c}return Aur(n,e,t)}case"function":{if(n!==void 0){var d=BP,h=t(n);return BP=d,oLe(n,e,h)}break}}var p=t;if(e==null)return p;var m=e[p];return m!==void 0?m:p}function Aur(n,e,t){var i="";if(Array.isArray(t))for(var r=0;r96?Hur:Bur},ANn=function(e,t,i){var r;if(t){var o=t.shouldForwardProp;r=e.__emotion_forwardProp&&o?function(l){return e.__emotion_forwardProp(l)&&o(l)}:o}return typeof r!="function"&&i&&(r=e.__emotion_forwardProp),r},Wur=function(e){var t=e.cache,i=e.serialized,r=e.isStringTag;return kzt(t,i,r),eci(function(){return Tzt(t,i,r)}),null},Vur=function n(e,t){var i=e.__emotion_real===e,r=i&&e.__emotion_base||e,o,l;t!==void 0&&(o=t.label,l=t.target);var c=ANn(e,t,i),d=c||INn(r),h=!d("as");return function(){var p=arguments,m=i&&e.__emotion_styles!==void 0?e.__emotion_styles.slice(0):[];if(o!==void 0&&m.push("label:"+o+";"),p[0]==null||p[0].raw===void 0)m.push.apply(m,p);else{var b=p[0];m.push(b[0]);for(var w=p.length,_=1;_e(zur(r)?t:r):e;return k.jsx(Fur,{styles:i})}function ici(n,e){return LOt(n,e)}function Uur(n,e){Array.isArray(n.__emotion_styles)&&(n.__emotion_styles=e(n.__emotion_styles))}const RNn=[];function fK(n){return RNn[0]=n,u5e(RNn)}const qur=n=>{const e=Object.keys(n).map(t=>({key:t,val:n[t]}))||[];return e.sort((t,i)=>t.val-i.val),e.reduce((t,i)=>({...t,[i.key]:i.val}),{})};function Gur(n){const{values:e={xs:0,sm:600,md:900,lg:1200,xl:1536},unit:t="px",step:i=5,...r}=n,o=qur(e),l=Object.keys(o);function c(b){return`@media (min-width:${typeof e[b]=="number"?e[b]:b}${t})`}function d(b){return`@media (max-width:${(typeof e[b]=="number"?e[b]:b)-i/100}${t})`}function h(b,w){const _=l.indexOf(w);return`@media (min-width:${typeof e[b]=="number"?e[b]:b}${t}) and (max-width:${(_!==-1&&typeof e[l[_]]=="number"?e[l[_]]:w)-i/100}${t})`}function p(b){return l.indexOf(b)+1(i.length===0?[1]:i).map(o=>{const l=e(o);return typeof l=="number"?`${l}px`:l}).join(" ");return t.mui=!0,t}function Yur(n,e){const t=this;if(t.vars){if(!t.colorSchemes?.[n]||typeof t.getColorSchemeSelector!="function")return{};let i=t.getColorSchemeSelector(n);return i==="&"?e:((i.includes("data-")||i.includes("."))&&(i=`*:where(${i.replace(/\s*&$/,"")}) &`),{[i]:e})}return t.palette.mode===n?e:{}}function h5e(n={},...e){const{breakpoints:t={},palette:i={},spacing:r,shape:o={},...l}=n,c=Gur(t),d=rci(r);let h=O_({breakpoints:c,direction:"ltr",components:{},palette:{mode:"light",...i},spacing:d,shape:{...Kur,...o}},l);return h=ncr(h),h.applyStyles=Yur,h=e.reduce((p,m)=>O_(p,m),h),h.unstable_sxConfig={...l5e,...l?.unstable_sxConfig},h.unstable_sx=function(m){return ZK({sx:m,theme:this})},h}function Zur(n){return Object.keys(n).length===0}function Ret(n=null){const e=D.useContext(d5e);return!e||Zur(e)?n:e}const Xur=h5e();function aoe(n=Xur){return Ret(n)}function EEt(n){const e=fK(n);return n!==e&&e.styles?(e.styles.match(/^@layer\s+[^{]*$/)||(e.styles=`@layer global{${e.styles}}`),e):n}function sci({styles:n,themeId:e,defaultTheme:t={}}){const i=aoe(t),r=e&&i[e]||i;let o=typeof n=="function"?n(r):n;return r.modularCssLayers&&(Array.isArray(o)?o=o.map(l=>EEt(typeof l=="function"?l(r):l)):o=EEt(o)),k.jsx(nci,{styles:o})}const MNn=n=>n,Qur=()=>{let n=MNn;return{configure(e){n=e},generate(e){return n(e)},reset(){n=MNn}}},Izt=Qur();function oci(n){var e,t,i="";if(typeof n=="string"||typeof n=="number")i+=n;else if(typeof n=="object")if(Array.isArray(n)){var r=n.length;for(e=0;ec!=="theme"&&c!=="sx"&&c!=="as"})(ZK);return D.forwardRef(function(d,h){const p=aoe(t),{className:m,component:b="div",...w}=Let(d);return k.jsx(o,{as:b,ref:h,className:_i(m,r?r(i):i),theme:e&&p[e]||p,...w})})}const Jur={active:"active",checked:"checked",completed:"completed",disabled:"disabled",error:"error",expanded:"expanded",focused:"focused",focusVisible:"focusVisible",open:"open",readOnly:"readOnly",required:"required",selected:"selected"};function No(n,e,t="Mui"){const i=Jur[e];return i?`${t}-${i}`:`${Izt.generate(n)}-${e}`}function Po(n,e,t="Mui"){const i={};return e.forEach(r=>{i[r]=No(n,r,t)}),i}const edr=Po("MuiBox",["root"]),xBe=aci({defaultClassName:edr.root,generateClassName:Izt.generate});function lci(n){const{variants:e,...t}=n,i={variants:e,style:fK(t),isProcessed:!0};return i.style===t||e&&e.forEach(r=>{typeof r.style!="function"&&(r.style=fK(r.style))}),i}const tdr=h5e();function kEt(n){return n!=="ownerState"&&n!=="theme"&&n!=="sx"&&n!=="as"}function $ne(n,e){return e&&n&&typeof n=="object"&&n.styles&&!n.styles.startsWith("@layer")&&(n.styles=`@layer ${e}{${String(n.styles)}}`),n}function ndr(n){return n?(e,t)=>t[n]:null}function idr(n,e,t){n.theme=sdr(n.theme)?t:n.theme[e]||n.theme}function pUe(n,e,t){const i=typeof e=="function"?e(n):e;if(Array.isArray(i))return i.flatMap(r=>pUe(n,r,t));if(Array.isArray(i?.variants)){let r;if(i.isProcessed)r=t?$ne(i.style,t):i.style;else{const{variants:o,...l}=i;r=t?$ne(fK(l),t):l}return cci(n,i.variants,[r],t)}return i?.isProcessed?t?$ne(fK(i.style),t):i.style:t?$ne(fK(i),t):i}function cci(n,e,t=[],i=void 0){let r;e:for(let o=0;o{Uur(c,F=>F.filter(j=>j!==ZK));const{name:h,slot:p,skipVariantsResolver:m,skipSx:b,overridesResolver:w=ndr(adr(p)),..._}=d,x=h&&h.startsWith("Mui")||p?"components":"custom",T=m!==void 0?m:p&&p!=="Root"&&p!=="root"||!1,I=b||!1;let L=kEt;p==="Root"||p==="root"?L=i:p?L=r:odr(c)&&(L=void 0);const A=ici(c,{shouldForwardProp:L,label:rdr(),..._}),M=F=>{if(F.__emotion_real===F)return F;if(typeof F=="function")return function(W){return pUe(W,F,W.theme.modularCssLayers?x:void 0)};if(KP(F)){const j=lci(F);return function(q){return j.variants?pUe(q,j,q.theme.modularCssLayers?x:void 0):q.theme.modularCssLayers?$ne(j.style,x):j.style}}return F},O=(...F)=>{const j=[],W=F.map(M),q=[];if(j.push(o),h&&w&&q.push(function(te){const ie=te.theme.components?.[h]?.styleOverrides;if(!ie)return null;const se={};for(const de in ie)se[de]=pUe(te,ie[de],te.theme.modularCssLayers?"theme":void 0);return w(te,se)}),h&&!T&&q.push(function(te){const ie=te.theme?.components?.[h]?.variants;return ie?cci(te,ie,[],te.theme.modularCssLayers?"theme":void 0):null}),I||q.push(ZK),Array.isArray(W[0])){const G=W.shift(),te=new Array(j.length).fill(""),Q=new Array(q.length).fill("");let ie;ie=[...te,...G,...Q],ie.raw=[...te,...G.raw,...Q],j.unshift(ie)}const Z=[...j,...W,...q],ee=A(...Z);return c.muiName&&(ee.muiName=c.muiName),ee};return A.withConfig&&(O.withConfig=A.withConfig),O}}function rdr(n,e){return void 0}function sdr(n){for(const e in n)return!1;return!0}function odr(n){return typeof n=="string"&&n.charCodeAt(0)>96}function adr(n){return n&&n.charAt(0).toLowerCase()+n.slice(1)}const loe=uci();function aLe(n,e,t=!1){const i={...e};for(const r in n)if(Object.prototype.hasOwnProperty.call(n,r)){const o=r;if(o==="components"||o==="slots")i[o]={...n[o],...i[o]};else if(o==="componentsProps"||o==="slotProps"){const l=n[o],c=e[o];if(!c)i[o]=l||{};else if(!l)i[o]=c;else{i[o]={...c};for(const d in l)if(Object.prototype.hasOwnProperty.call(l,d)){const h=d;i[o][h]=aLe(l[h],c[h],t)}}}else o==="className"&&t&&e.className?i.className=_i(n?.className,e?.className):o==="style"&&t&&e.style?i.style={...n?.style,...e?.style}:i[o]===void 0&&(i[o]=n[o])}return i}function dci(n){const{theme:e,name:t,props:i}=n;return!e||!e.components||!e.components[t]||!e.components[t].defaultProps?i:aLe(e.components[t].defaultProps,i)}function Azt({props:n,name:e,defaultTheme:t,themeId:i}){let r=aoe(t);return i&&(r=r[i]||r),dci({theme:r,name:e,props:n})}const IS=typeof window<"u"?D.useLayoutEffect:D.useEffect;function ldr(n,e,t,i,r){const[o,l]=D.useState(()=>r&&t?t(n).matches:i?i(n).matches:e);return IS(()=>{if(!t)return;const c=t(n),d=()=>{l(c.matches)};return d(),c.addEventListener("change",d),()=>{c.removeEventListener("change",d)}},[n,t]),o}const cdr={...V9},hci=cdr.useSyncExternalStore;function udr(n,e,t,i,r){const o=D.useCallback(()=>e,[e]),l=D.useMemo(()=>{if(r&&t)return()=>t(n).matches;if(i!==null){const{matches:p}=i(n);return()=>p}return o},[o,n,i,r,t]),[c,d]=D.useMemo(()=>{if(t===null)return[o,()=>()=>{}];const p=t(n);return[()=>p.matches,m=>(p.addEventListener("change",m),()=>{p.removeEventListener("change",m)})]},[o,t,n]);return hci(d,c,l)}function fci(n={}){const{themeId:e}=n;return function(i,r={}){let o=Ret();o&&e&&(o=o[e]||o);const l=typeof window<"u"&&typeof window.matchMedia<"u",{defaultMatches:c=!1,matchMedia:d=l?window.matchMedia:null,ssrMatchMedia:h=null,noSsr:p=!1}=dci({name:"MuiUseMediaQuery",props:r,theme:o});let m=typeof i=="function"?i(o):i;return m=m.replace(/^@media( ?)/m,""),m.includes("print")&&console.warn(["MUI: You have provided a `print` query to the `useMediaQuery` hook.","Using the print media query to modify print styles can lead to unexpected results.","Consider using the `displayPrint` field in the `sx` prop instead.","More information about `displayPrint` on our docs: https://mui.com/system/display/#display-in-print."].join(` -`)),(hci!==void 0?udr:ldr)(m,c,d,h,p)}}fci();function ffe(n,e=Number.MIN_SAFE_INTEGER,t=Number.MAX_SAFE_INTEGER){return Math.max(e,Math.min(n,t))}function Rzt(n,e=0,t=1){return ffe(n,e,t)}function ddr(n){n=n.slice(1);const e=new RegExp(`.{1,${n.length>=6?2:1}}`,"g");let t=n.match(e);return t&&t[0].length===1&&(t=t.map(i=>i+i)),t?`rgb${t.length===4?"a":""}(${t.map((i,r)=>r<3?parseInt(i,16):Math.round(parseInt(i,16)/255*1e3)/1e3).join(", ")})`:""}function XK(n){if(n.type)return n;if(n.charAt(0)==="#")return XK(ddr(n));const e=n.indexOf("("),t=n.substring(0,e);if(!["rgb","rgba","hsl","hsla","color"].includes(t))throw new Error(bW(9,n));let i=n.substring(e+1,n.length-1),r;if(t==="color"){if(i=i.split(" "),r=i.shift(),i.length===4&&i[3].charAt(0)==="/"&&(i[3]=i[3].slice(1)),!["srgb","display-p3","a98-rgb","prophoto-rgb","rec-2020"].includes(r))throw new Error(bW(10,r))}else i=i.split(",");return i=i.map(o=>parseFloat(o)),{type:t,values:i,colorSpace:r}}const hdr=n=>{const e=XK(n);return e.values.slice(0,3).map((t,i)=>e.type.includes("hsl")&&i!==0?`${t}%`:t).join(" ")},ake=(n,e)=>{try{return hdr(n)}catch{return n}};function Met(n){const{type:e,colorSpace:t}=n;let{values:i}=n;return e.includes("rgb")?i=i.map((r,o)=>o<3?parseInt(r,10):r):e.includes("hsl")&&(i[1]=`${i[1]}%`,i[2]=`${i[2]}%`),e.includes("color")?i=`${t} ${i.join(" ")}`:i=`${i.join(", ")}`,`${e}(${i})`}function pci(n){n=XK(n);const{values:e}=n,t=e[0],i=e[1]/100,r=e[2]/100,o=i*Math.min(r,1-r),l=(h,p=(h+t/30)%12)=>r-o*Math.max(Math.min(p-3,9-p,1),-1);let c="rgb";const d=[Math.round(l(0)*255),Math.round(l(8)*255),Math.round(l(4)*255)];return n.type==="hsla"&&(c+="a",d.push(e[3])),Met({type:c,values:d})}function DOt(n){n=XK(n);let e=n.type==="hsl"||n.type==="hsla"?XK(pci(n)).values:n.values;return e=e.map(t=>(n.type!=="color"&&(t/=255),t<=.03928?t/12.92:((t+.055)/1.055)**2.4)),Number((.2126*e[0]+.7152*e[1]+.0722*e[2]).toFixed(3))}function fdr(n,e){const t=DOt(n),i=DOt(e);return(Math.max(t,i)+.05)/(Math.min(t,i)+.05)}function Wa(n,e){return n=XK(n),e=Rzt(e),(n.type==="rgb"||n.type==="hsl")&&(n.type+="a"),n.type==="color"?n.values[3]=`/${e}`:n.values[3]=e,Met(n)}function hte(n,e,t){try{return Wa(n,e)}catch{return n}}function Oet(n,e){if(n=XK(n),e=Rzt(e),n.type.includes("hsl"))n.values[2]*=1-e;else if(n.type.includes("rgb")||n.type.includes("color"))for(let t=0;t<3;t+=1)n.values[t]*=1-e;return Met(n)}function qp(n,e,t){try{return Oet(n,e)}catch{return n}}function Net(n,e){if(n=XK(n),e=Rzt(e),n.type.includes("hsl"))n.values[2]+=(100-n.values[2])*e;else if(n.type.includes("rgb"))for(let t=0;t<3;t+=1)n.values[t]+=(255-n.values[t])*e;else if(n.type.includes("color"))for(let t=0;t<3;t+=1)n.values[t]+=(1-n.values[t])*e;return Met(n)}function Gp(n,e,t){try{return Net(n,e)}catch{return n}}function lLe(n,e=.15){return DOt(n)>.5?Oet(n,e):Net(n,e)}function EBe(n,e,t){try{return lLe(n,e)}catch{return n}}const gci=D.createContext(null);function Mzt(){return D.useContext(gci)}const pdr=typeof Symbol=="function"&&Symbol.for,gdr=pdr?Symbol.for("mui.nested"):"__THEME_NESTED__";function mdr(n,e){return typeof e=="function"?e(n):{...n,...e}}function bdr(n){const{children:e,theme:t}=n,i=Mzt(),r=D.useMemo(()=>{const o=i===null?{...t}:mdr(i,t);return o!=null&&(o[gdr]=i!==null),o},[t,i]);return k.jsx(gci.Provider,{value:r,children:e})}const mci=D.createContext();function vdr({value:n,...e}){return k.jsx(mci.Provider,{value:n??!0,...e})}const MY=()=>D.useContext(mci)??!1,bci=D.createContext(void 0);function wdr({value:n,children:e}){return k.jsx(bci.Provider,{value:n,children:e})}function ydr(n){const{theme:e,name:t,props:i}=n;if(!e||!e.components||!e.components[t])return i;const r=e.components[t];return r.defaultProps?aLe(r.defaultProps,i,e.components.mergeClassNameAndStyle):!r.styleOverrides&&!r.variants?aLe(r,i,e.components.mergeClassNameAndStyle):i}function _dr({props:n,name:e}){const t=D.useContext(bci);return ydr({props:n,name:e,theme:{components:t}})}let ONn=0;function Cdr(n){const[e,t]=D.useState(n),i=n||e;return D.useEffect(()=>{e==null&&(ONn+=1,t(`mui-${ONn}`))},[e]),i}const Sdr={...V9},NNn=Sdr.useId;function YW(n){if(NNn!==void 0){const e=NNn();return n??e}return Cdr(n)}function xdr(n){const e=Ret(),t=YW()||"",{modularCssLayers:i}=n;let r="mui.global, mui.components, mui.theme, mui.custom, mui.sx";return!i||e!==null?r="":typeof i=="string"?r=i.replace(/mui(?!\.)/g,r):r=`@layer ${r};`,IS(()=>{const o=document.querySelector("head");if(!o)return;const l=o.firstChild;if(r){if(l&&l.hasAttribute?.("data-mui-layer-order")&&l.getAttribute("data-mui-layer-order")===t)return;const c=document.createElement("style");c.setAttribute("data-mui-layer-order",t),c.textContent=r,o.prepend(c)}else o.querySelector(`style[data-mui-layer-order="${t}"]`)?.remove()},[r,t]),r?k.jsx(sci,{styles:r}):null}const PNn={};function FNn(n,e,t,i=!1){return D.useMemo(()=>{const r=n&&e[n]||e;if(typeof t=="function"){const o=t(r),l=n?{...e,[n]:o}:o;return i?()=>l:l}return n?{...e,[n]:t}:{...e,...t}},[n,e,t,i])}function vci(n){const{children:e,theme:t,themeId:i}=n,r=Ret(PNn),o=Mzt()||PNn,l=FNn(i,r,t),c=FNn(i,o,t,!0),d=(i?l[i]:l).direction==="rtl",h=xdr(l);return k.jsx(bdr,{theme:c,children:k.jsx(d5e.Provider,{value:l,children:k.jsx(vdr,{value:d,children:k.jsxs(wdr,{value:i?l[i].components:l.components,children:[h,e]})})})})}const jNn={theme:void 0};function Edr(n){let e,t;return function(r){let o=e;return(o===void 0||r.theme!==t)&&(jNn.theme=r.theme,o=lci(n(jNn)),e=o,t=r.theme),o}}const Ozt="mode",Nzt="color-scheme",kdr="data-color-scheme";function Tdr(n){const{defaultMode:e="system",defaultLightColorScheme:t="light",defaultDarkColorScheme:i="dark",modeStorageKey:r=Ozt,colorSchemeStorageKey:o=Nzt,attribute:l=kdr,colorSchemeNode:c="document.documentElement",nonce:d}=n||{};let h="",p=l;if(l==="class"&&(p=".%s"),l==="data"&&(p="[data-%s]"),p.startsWith(".")){const b=p.substring(1);h+=`${c}.classList.remove('${b}'.replace('%s', light), '${b}'.replace('%s', dark)); +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/cssMode-Bg4Vg_j9.js","assets/lspLanguageFeatures-WUkMtvXB.js","assets/htmlMode-Dcv1flv1.js","assets/jsonMode-CtloMU4M.js","assets/javascript-CPKTHyFs.js","assets/typescript-SURKSaZv.js"])))=>i.map(i=>d[i]); +function Xlr(n,e){for(var t=0;ti[r]})}}}return Object.freeze(Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))i(r);new MutationObserver(r=>{for(const o of r)if(o.type==="childList")for(const l of o.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&i(l)}).observe(document,{childList:!0,subtree:!0});function t(r){const o={};return r.integrity&&(o.integrity=r.integrity),r.referrerPolicy&&(o.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?o.credentials="include":r.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function i(r){if(r.ep)return;r.ep=!0;const o=t(r);fetch(r.href,o)}})();var Sy=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Vs(n){return n&&n.__esModule&&Object.prototype.hasOwnProperty.call(n,"default")?n.default:n}function s5e(n){if(Object.prototype.hasOwnProperty.call(n,"__esModule"))return n;var e=n.default;if(typeof e=="function"){var t=function i(){var r=!1;try{r=this instanceof i}catch{}return r?Reflect.construct(e,arguments,this.constructor):e.apply(this,arguments)};t.prototype=e.prototype}else t={};return Object.defineProperty(t,"__esModule",{value:!0}),Object.keys(n).forEach(function(i){var r=Object.getOwnPropertyDescriptor(n,i);Object.defineProperty(t,i,r.get?r:{enumerable:!0,get:function(){return n[i]}})}),t}var gEt={exports:{}},Axe={},mEt={exports:{}},wd={};var lNn;function Qlr(){if(lNn)return wd;lNn=1;var n=Symbol.for("react.element"),e=Symbol.for("react.portal"),t=Symbol.for("react.fragment"),i=Symbol.for("react.strict_mode"),r=Symbol.for("react.profiler"),o=Symbol.for("react.provider"),l=Symbol.for("react.context"),c=Symbol.for("react.forward_ref"),d=Symbol.for("react.suspense"),h=Symbol.for("react.memo"),p=Symbol.for("react.lazy"),m=Symbol.iterator;function b(ce){return ce===null||typeof ce!="object"?null:(ce=m&&ce[m]||ce["@@iterator"],typeof ce=="function"?ce:null)}var w={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_=Object.assign,x={};function T(ce,ye,he){this.props=ce,this.context=ye,this.refs=x,this.updater=he||w}T.prototype.isReactComponent={},T.prototype.setState=function(ce,ye){if(typeof ce!="object"&&typeof ce!="function"&&ce!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,ce,ye,"setState")},T.prototype.forceUpdate=function(ce){this.updater.enqueueForceUpdate(this,ce,"forceUpdate")};function I(){}I.prototype=T.prototype;function D(ce,ye,he){this.props=ce,this.context=ye,this.refs=x,this.updater=he||w}var A=D.prototype=new I;A.constructor=D,_(A,T.prototype),A.isPureReactComponent=!0;var M=Array.isArray,O=Object.prototype.hasOwnProperty,F={current:null},j={key:!0,ref:!0,__self:!0,__source:!0};function W(ce,ye,he){var pe,me={},be=null,xe=null;if(ye!=null)for(pe in ye.ref!==void 0&&(xe=ye.ref),ye.key!==void 0&&(be=""+ye.key),ye)O.call(ye,pe)&&!j.hasOwnProperty(pe)&&(me[pe]=ye[pe]);var Te=arguments.length-2;if(Te===1)me.children=he;else if(1t.searchParams.append("args[]",i)),`Minified MUI error #${n}; visit ${t} for the full message.`}function ri(n){if(typeof n!="string")throw new Error(mW(7));return n.charAt(0).toUpperCase()+n.slice(1)}var yEt={exports:{}},yg={};var gNn;function rcr(){if(gNn)return yg;gNn=1;var n=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),t=Symbol.for("react.fragment"),i=Symbol.for("react.strict_mode"),r=Symbol.for("react.profiler"),o=Symbol.for("react.consumer"),l=Symbol.for("react.context"),c=Symbol.for("react.forward_ref"),d=Symbol.for("react.suspense"),h=Symbol.for("react.suspense_list"),p=Symbol.for("react.memo"),m=Symbol.for("react.lazy"),b=Symbol.for("react.view_transition"),w=Symbol.for("react.client.reference");function _(x){if(typeof x=="object"&&x!==null){var T=x.$$typeof;switch(T){case n:switch(x=x.type,x){case t:case r:case i:case d:case h:case b:return x;default:switch(x=x&&x.$$typeof,x){case l:case c:case m:case p:return x;case o:return x;default:return T}}case e:return T}}}return yg.ContextConsumer=o,yg.ContextProvider=l,yg.Element=n,yg.ForwardRef=c,yg.Fragment=t,yg.Lazy=m,yg.Memo=p,yg.Portal=e,yg.Profiler=r,yg.StrictMode=i,yg.Suspense=d,yg.SuspenseList=h,yg.isContextConsumer=function(x){return _(x)===o},yg.isContextProvider=function(x){return _(x)===l},yg.isElement=function(x){return typeof x=="object"&&x!==null&&x.$$typeof===n},yg.isForwardRef=function(x){return _(x)===c},yg.isFragment=function(x){return _(x)===t},yg.isLazy=function(x){return _(x)===m},yg.isMemo=function(x){return _(x)===p},yg.isPortal=function(x){return _(x)===e},yg.isProfiler=function(x){return _(x)===r},yg.isStrictMode=function(x){return _(x)===i},yg.isSuspense=function(x){return _(x)===d},yg.isSuspenseList=function(x){return _(x)===h},yg.isValidElementType=function(x){return typeof x=="string"||typeof x=="function"||x===t||x===r||x===i||x===d||x===h||typeof x=="object"&&x!==null&&(x.$$typeof===m||x.$$typeof===p||x.$$typeof===l||x.$$typeof===o||x.$$typeof===c||x.$$typeof===w||x.getModuleId!==void 0)},yg.typeOf=_,yg}var mNn;function scr(){return mNn||(mNn=1,yEt.exports=rcr()),yEt.exports}var wet=scr();function ZP(n){if(typeof n!="object"||n===null)return!1;const e=Object.getPrototypeOf(n);return(e===null||e===Object.prototype||Object.getPrototypeOf(e)===null)&&!(Symbol.toStringTag in n)&&!(Symbol.iterator in n)}function $li(n){if(L.isValidElement(n)||wet.isValidElementType(n)||!ZP(n))return n;const e={};return Object.keys(n).forEach(t=>{e[t]=$li(n[t])}),e}function F_(n,e,t={clone:!0}){const i=t.clone?{...n}:n;return ZP(n)&&ZP(e)&&Object.keys(e).forEach(r=>{L.isValidElement(e[r])||wet.isValidElementType(e[r])?i[r]=e[r]:ZP(e[r])&&Object.prototype.hasOwnProperty.call(n,r)&&ZP(n[r])?i[r]=F_(n[r],e[r],t):t.clone?i[r]=ZP(e[r])?$li(e[r]):e[r]:i[r]=e[r]}),i}function Jke(n,e){return e?F_(n,e,{clone:!1}):n}function bNn(n,e){if(!n.containerQueries)return e;const t=Object.keys(e).filter(i=>i.startsWith("@container")).sort((i,r)=>{const o=/min-width:\s*([0-9.]+)/;return+(i.match(o)?.[1]||0)-+(r.match(o)?.[1]||0)});return t.length?t.reduce((i,r)=>{const o=e[r];return delete i[r],i[r]=o,i},{...e}):e}function ocr(n,e){return e==="@"||e.startsWith("@")&&(n.some(t=>e.startsWith(`@${t}`))||!!e.match(/^@\d/))}function acr(n,e){const t=e.match(/^@([^/]+)?\/?(.+)?$/);if(!t)return null;const[,i,r]=t,o=Number.isNaN(+i)?i||0:+i;return n.containerQueries(r).up(o)}function lcr(n){const e=(o,l)=>o.replace("@media",l?`@container ${l}`:"@container");function t(o,l){o.up=(...c)=>e(n.breakpoints.up(...c),l),o.down=(...c)=>e(n.breakpoints.down(...c),l),o.between=(...c)=>e(n.breakpoints.between(...c),l),o.only=(...c)=>e(n.breakpoints.only(...c),l),o.not=(...c)=>{const d=e(n.breakpoints.not(...c),l);return d.includes("not all and")?d.replace("not all and ","").replace("min-width:","width<").replace("max-width:","width>").replace("and","or"):d}}const i={},r=o=>(t(i,o),i);return t(r),{...n,containerQueries:r}}const yet={xs:0,sm:600,md:900,lg:1200,xl:1536},vNn={keys:["xs","sm","md","lg","xl"],up:n=>`@media (min-width:${yet[n]}px)`},ccr={containerQueries:n=>({up:e=>{let t=typeof e=="number"?e:yet[e]||e;return typeof t=="number"&&(t=`${t}px`),n?`@container ${n} (min-width:${t})`:`@container (min-width:${t})`}})};function dM(n,e,t){const i=n.theme||{};if(Array.isArray(e)){const o=i.breakpoints||vNn;return e.reduce((l,c,d)=>(l[o.up(o.keys[d])]=t(e[d]),l),{})}if(typeof e=="object"){const o=i.breakpoints||vNn;return Object.keys(e).reduce((l,c)=>{if(ocr(o.keys,c)){const d=acr(i.containerQueries?i:ccr,c);d&&(l[d]=t(e[c],c))}else if(Object.keys(o.values||yet).includes(c)){const d=o.up(c);l[d]=t(e[c],c)}else{const d=c;l[d]=e[d]}return l},{})}return t(e)}function zli(n={}){return n.keys?.reduce((t,i)=>{const r=n.up(i);return t[r]={},t},{})||{}}function COt(n,e){return n.reduce((t,i)=>{const r=t[i];return(!r||Object.keys(r).length===0)&&delete t[i],t},e)}function ucr(n,...e){const t=zli(n),i=[t,...e].reduce((r,o)=>F_(r,o),{});return COt(Object.keys(t),i)}function dcr(n,e){if(typeof n!="object")return{};const t={},i=Object.keys(e);return Array.isArray(n)?i.forEach((r,o)=>{o{n[r]!=null&&(t[r]=!0)}),t}function _Et({values:n,breakpoints:e,base:t}){const i=t||dcr(n,e),r=Object.keys(i);if(r.length===0)return n;let o;return r.reduce((l,c,d)=>(Array.isArray(n)?(l[c]=n[d]!=null?n[d]:n[o],o=d):typeof n=="object"?(l[c]=n[c]!=null?n[c]:n[o],o=c):l[c]=n,l),{})}function BP(n,e,t=!0){if(!e||typeof e!="string")return null;if(n&&n.vars&&t){const i=`vars.${e}`.split(".").reduce((r,o)=>r&&r[o]?r[o]:null,n);if(i!=null)return i}return e.split(".").reduce((i,r)=>i&&i[r]!=null?i[r]:null,n)}function Gqe(n,e,t,i=t){let r;return typeof n=="function"?r=n(t):Array.isArray(n)?r=n[t]||i:r=BP(n,t)||i,e&&(r=e(r,i,n)),r}function wv(n){const{prop:e,cssProperty:t=n.prop,themeKey:i,transform:r}=n,o=l=>{if(l[e]==null)return null;const c=l[e],d=l.theme,h=BP(d,i)||{};return dM(l,c,m=>{let b=Gqe(h,r,m);return m===b&&typeof m=="string"&&(b=Gqe(h,r,`${e}${m==="default"?"":ri(m)}`,m)),t===!1?b:{[t]:b}})};return o.propTypes={},o.filterProps=[e],o}function hcr(n){const e={};return t=>(e[t]===void 0&&(e[t]=n(t)),e[t])}const fcr={m:"margin",p:"padding"},pcr={t:"Top",r:"Right",b:"Bottom",l:"Left",x:["Left","Right"],y:["Top","Bottom"]},wNn={marginX:"mx",marginY:"my",paddingX:"px",paddingY:"py"},gcr=hcr(n=>{if(n.length>2)if(wNn[n])n=wNn[n];else return[n];const[e,t]=n.split(""),i=fcr[e],r=pcr[t]||"";return Array.isArray(r)?r.map(o=>i+o):[i+r]}),vzt=["m","mt","mr","mb","ml","mx","my","margin","marginTop","marginRight","marginBottom","marginLeft","marginX","marginY","marginInline","marginInlineStart","marginInlineEnd","marginBlock","marginBlockStart","marginBlockEnd"],wzt=["p","pt","pr","pb","pl","px","py","padding","paddingTop","paddingRight","paddingBottom","paddingLeft","paddingX","paddingY","paddingInline","paddingInlineStart","paddingInlineEnd","paddingBlock","paddingBlockStart","paddingBlockEnd"];[...vzt,...wzt];function o5e(n,e,t,i){const r=BP(n,e,!0)??t;return typeof r=="number"||typeof r=="string"?o=>typeof o=="string"?o:typeof r=="string"?r.startsWith("var(")&&o===0?0:r.startsWith("var(")&&o===1?r:`calc(${o} * ${r})`:r*o:Array.isArray(r)?o=>{if(typeof o=="string")return o;const l=Math.abs(o),c=r[l];return o>=0?c:typeof c=="number"?-c:typeof c=="string"&&c.startsWith("var(")?`calc(-1 * ${c})`:`-${c}`}:typeof r=="function"?r:()=>{}}function _et(n){return o5e(n,"spacing",8)}function Xre(n,e){return typeof e=="string"||e==null?e:n(e)}function mcr(n,e){return t=>n.reduce((i,r)=>(i[r]=Xre(e,t),i),{})}function bcr(n,e,t,i){if(!e.includes(t))return null;const r=gcr(t),o=mcr(r,i),l=n[t];return dM(n,l,o)}function Uli(n,e){const t=_et(n.theme);return Object.keys(n).map(i=>bcr(n,e,i,t)).reduce(Jke,{})}function nb(n){return Uli(n,vzt)}nb.propTypes={};nb.filterProps=vzt;function ib(n){return Uli(n,wzt)}ib.propTypes={};ib.filterProps=wzt;function Cet(...n){const e=n.reduce((i,r)=>(r.filterProps.forEach(o=>{i[o]=r}),i),{}),t=i=>Object.keys(i).reduce((r,o)=>e[o]?Jke(r,e[o](i)):r,{});return t.propTypes={},t.filterProps=n.reduce((i,r)=>i.concat(r.filterProps),[]),t}function n3(n){return typeof n!="number"?n:`${n}px solid`}function B3(n,e){return wv({prop:n,themeKey:"borders",transform:e})}const vcr=B3("border",n3),wcr=B3("borderTop",n3),ycr=B3("borderRight",n3),_cr=B3("borderBottom",n3),Ccr=B3("borderLeft",n3),Scr=B3("borderColor"),xcr=B3("borderTopColor"),Ecr=B3("borderRightColor"),kcr=B3("borderBottomColor"),Tcr=B3("borderLeftColor"),Lcr=B3("outline",n3),Dcr=B3("outlineColor"),xet=n=>{if(n.borderRadius!==void 0&&n.borderRadius!==null){const e=o5e(n.theme,"shape.borderRadius",4),t=i=>({borderRadius:Xre(e,i)});return dM(n,n.borderRadius,t)}return null};xet.propTypes={};xet.filterProps=["borderRadius"];Cet(vcr,wcr,ycr,_cr,Ccr,Scr,xcr,Ecr,kcr,Tcr,xet,Lcr,Dcr);const Eet=n=>{if(n.gap!==void 0&&n.gap!==null){const e=o5e(n.theme,"spacing",8),t=i=>({gap:Xre(e,i)});return dM(n,n.gap,t)}return null};Eet.propTypes={};Eet.filterProps=["gap"];const ket=n=>{if(n.columnGap!==void 0&&n.columnGap!==null){const e=o5e(n.theme,"spacing",8),t=i=>({columnGap:Xre(e,i)});return dM(n,n.columnGap,t)}return null};ket.propTypes={};ket.filterProps=["columnGap"];const Tet=n=>{if(n.rowGap!==void 0&&n.rowGap!==null){const e=o5e(n.theme,"spacing",8),t=i=>({rowGap:Xre(e,i)});return dM(n,n.rowGap,t)}return null};Tet.propTypes={};Tet.filterProps=["rowGap"];const Icr=wv({prop:"gridColumn"}),Acr=wv({prop:"gridRow"}),Rcr=wv({prop:"gridAutoFlow"}),Mcr=wv({prop:"gridAutoColumns"}),Ocr=wv({prop:"gridAutoRows"}),Ncr=wv({prop:"gridTemplateColumns"}),Pcr=wv({prop:"gridTemplateRows"}),Fcr=wv({prop:"gridTemplateAreas"}),jcr=wv({prop:"gridArea"});Cet(Eet,ket,Tet,Icr,Acr,Rcr,Mcr,Ocr,Ncr,Pcr,Fcr,jcr);function qpe(n,e){return e==="grey"?e:n}const Hcr=wv({prop:"color",themeKey:"palette",transform:qpe}),Bcr=wv({prop:"bgcolor",cssProperty:"backgroundColor",themeKey:"palette",transform:qpe}),Wcr=wv({prop:"backgroundColor",themeKey:"palette",transform:qpe});Cet(Hcr,Bcr,Wcr);function LL(n){return n<=1&&n!==0?`${n*100}%`:n}const Vcr=wv({prop:"width",transform:LL}),yzt=n=>{if(n.maxWidth!==void 0&&n.maxWidth!==null){const e=t=>{const i=n.theme?.breakpoints?.values?.[t]||yet[t];return i?n.theme?.breakpoints?.unit!=="px"?{maxWidth:`${i}${n.theme.breakpoints.unit}`}:{maxWidth:i}:{maxWidth:LL(t)}};return dM(n,n.maxWidth,e)}return null};yzt.filterProps=["maxWidth"];const $cr=wv({prop:"minWidth",transform:LL}),zcr=wv({prop:"height",transform:LL}),Ucr=wv({prop:"maxHeight",transform:LL}),qcr=wv({prop:"minHeight",transform:LL});wv({prop:"size",cssProperty:"width",transform:LL});wv({prop:"size",cssProperty:"height",transform:LL});const Gcr=wv({prop:"boxSizing"});Cet(Vcr,yzt,$cr,zcr,Ucr,qcr,Gcr);const a5e={border:{themeKey:"borders",transform:n3},borderTop:{themeKey:"borders",transform:n3},borderRight:{themeKey:"borders",transform:n3},borderBottom:{themeKey:"borders",transform:n3},borderLeft:{themeKey:"borders",transform:n3},borderColor:{themeKey:"palette"},borderTopColor:{themeKey:"palette"},borderRightColor:{themeKey:"palette"},borderBottomColor:{themeKey:"palette"},borderLeftColor:{themeKey:"palette"},outline:{themeKey:"borders",transform:n3},outlineColor:{themeKey:"palette"},borderRadius:{themeKey:"shape.borderRadius",style:xet},color:{themeKey:"palette",transform:qpe},bgcolor:{themeKey:"palette",cssProperty:"backgroundColor",transform:qpe},backgroundColor:{themeKey:"palette",transform:qpe},p:{style:ib},pt:{style:ib},pr:{style:ib},pb:{style:ib},pl:{style:ib},px:{style:ib},py:{style:ib},padding:{style:ib},paddingTop:{style:ib},paddingRight:{style:ib},paddingBottom:{style:ib},paddingLeft:{style:ib},paddingX:{style:ib},paddingY:{style:ib},paddingInline:{style:ib},paddingInlineStart:{style:ib},paddingInlineEnd:{style:ib},paddingBlock:{style:ib},paddingBlockStart:{style:ib},paddingBlockEnd:{style:ib},m:{style:nb},mt:{style:nb},mr:{style:nb},mb:{style:nb},ml:{style:nb},mx:{style:nb},my:{style:nb},margin:{style:nb},marginTop:{style:nb},marginRight:{style:nb},marginBottom:{style:nb},marginLeft:{style:nb},marginX:{style:nb},marginY:{style:nb},marginInline:{style:nb},marginInlineStart:{style:nb},marginInlineEnd:{style:nb},marginBlock:{style:nb},marginBlockStart:{style:nb},marginBlockEnd:{style:nb},displayPrint:{cssProperty:!1,transform:n=>({"@media print":{display:n}})},display:{},overflow:{},textOverflow:{},visibility:{},whiteSpace:{},flexBasis:{},flexDirection:{},flexWrap:{},justifyContent:{},alignItems:{},alignContent:{},order:{},flex:{},flexGrow:{},flexShrink:{},alignSelf:{},justifyItems:{},justifySelf:{},gap:{style:Eet},rowGap:{style:Tet},columnGap:{style:ket},gridColumn:{},gridRow:{},gridAutoFlow:{},gridAutoColumns:{},gridAutoRows:{},gridTemplateColumns:{},gridTemplateRows:{},gridTemplateAreas:{},gridArea:{},position:{},zIndex:{themeKey:"zIndex"},top:{},right:{},bottom:{},left:{},boxShadow:{themeKey:"shadows"},width:{transform:LL},maxWidth:{style:yzt},minWidth:{transform:LL},height:{transform:LL},maxHeight:{transform:LL},minHeight:{transform:LL},boxSizing:{},font:{themeKey:"font"},fontFamily:{themeKey:"typography"},fontSize:{themeKey:"typography"},fontStyle:{themeKey:"typography"},fontWeight:{themeKey:"typography"},letterSpacing:{},textTransform:{},lineHeight:{},textAlign:{},typography:{cssProperty:!1,themeKey:"typography"}};function Kcr(...n){const e=n.reduce((i,r)=>i.concat(Object.keys(r)),[]),t=new Set(e);return n.every(i=>t.size===Object.keys(i).length)}function Ycr(n,e){return typeof n=="function"?n(e):n}function Zcr(){function n(t,i,r,o){const l={[t]:i,theme:r},c=o[t];if(!c)return{[t]:i};const{cssProperty:d=t,themeKey:h,transform:p,style:m}=c;if(i==null)return null;if(h==="typography"&&i==="inherit")return{[t]:i};const b=BP(r,h)||{};return m?m(l):dM(l,i,_=>{let x=Gqe(b,p,_);return _===x&&typeof _=="string"&&(x=Gqe(b,p,`${t}${_==="default"?"":ri(_)}`,_)),d===!1?x:{[d]:x}})}function e(t){const{sx:i,theme:r={},nested:o}=t||{};if(!i)return null;const l=r.unstable_sxConfig??a5e;function c(d){let h=d;if(typeof d=="function")h=d(r);else if(typeof d!="object")return d;if(!h)return null;const p=zli(r.breakpoints),m=Object.keys(p);let b=p;return Object.keys(h).forEach(w=>{const _=Ycr(h[w],r);if(_!=null)if(typeof _=="object")if(l[w])b=Jke(b,n(w,_,r,l));else{const x=dM({theme:r},_,T=>({[w]:T}));Kcr(x,_)?b[w]=e({sx:_,theme:r,nested:!0}):b=Jke(b,x)}else b=Jke(b,n(w,_,r,l))}),!o&&r.modularCssLayers?{"@layer sx":bNn(r,COt(m,b))}:bNn(r,COt(m,b))}return Array.isArray(i)?i.map(c):c(i)}return e}const XK=Zcr();XK.filterProps=["sx"];const Xcr=n=>{const e={systemProps:{},otherProps:{}},t=n?.theme?.unstable_sxConfig??a5e;return Object.keys(n).forEach(i=>{t[i]?e.systemProps[i]=n[i]:e.otherProps[i]=n[i]}),e};function Let(n){const{sx:e,...t}=n,{systemProps:i,otherProps:r}=Xcr(t);let o;return Array.isArray(e)?o=[i,...e]:typeof e=="function"?o=(...l)=>{const c=e(...l);return ZP(c)?{...i,...c}:i}:o={...i,...e},{...r,sx:o}}function Zt(){return Zt=Object.assign?Object.assign.bind():function(n){for(var e=1;e0?G2(B1e,--rT):0,cme--,rw===10&&(cme=1,Iet--),rw}function HL(){return rw=rT2||i6e(rw)>3?"":" "}function dur(n,e){for(;--e&&HL()&&!(rw<48||rw>102||rw>57&&rw<65||rw>70&&rw<97););return l5e(n,dUe()+(e<6&&x9()==32&&HL()==32))}function xOt(n){for(;HL();)switch(rw){case n:return rT;case 34:case 39:n!==34&&n!==39&&xOt(rw);break;case 40:n===41&&xOt(n);break;case 92:HL();break}return rT}function hur(n,e){for(;HL()&&n+rw!==57;)if(n+rw===84&&x9()===47)break;return"/*"+l5e(e,rT-1)+"*"+Det(n===47?n:HL())}function fur(n){for(;!i6e(x9());)HL();return l5e(n,rT)}function pur(n){return Xli(fUe("",null,null,null,[""],n=Zli(n),0,[0],n))}function fUe(n,e,t,i,r,o,l,c,d){for(var h=0,p=0,m=l,b=0,w=0,_=0,x=1,T=1,I=1,D=0,A="",M=r,O=o,F=i,j=A;T;)switch(_=D,D=HL()){case 40:if(_!=108&&G2(j,m-1)==58){SOt(j+=zf(hUe(D),"&","&\f"),"&\f")!=-1&&(I=-1);break}case 34:case 39:case 91:j+=hUe(D);break;case 9:case 10:case 13:case 32:j+=uur(_);break;case 92:j+=dur(dUe()-1,7);continue;case 47:switch(x9()){case 42:case 47:SBe(gur(hur(HL(),dUe()),e,t),d);break;default:j+="/"}break;case 123*x:c[h++]=WP(j)*I;case 125*x:case 59:case 0:switch(D){case 0:case 125:T=0;case 59+p:I==-1&&(j=zf(j,/\f/g,"")),w>0&&WP(j)-m&&SBe(w>32?_Nn(j+";",i,t,m-1):_Nn(zf(j," ","")+";",i,t,m-2),d);break;case 59:j+=";";default:if(SBe(F=yNn(j,e,t,h,p,r,c,A,M=[],O=[],m),o),D===123)if(p===0)fUe(j,e,F,F,M,o,m,c,O);else switch(b===99&&G2(j,3)===110?100:b){case 100:case 108:case 109:case 115:fUe(n,F,F,i&&SBe(yNn(n,F,F,0,0,r,c,A,r,M=[],m),O),r,O,m,c,i?M:O);break;default:fUe(j,F,F,F,[""],O,0,c,O)}}h=p=w=0,x=I=1,A=j="",m=l;break;case 58:m=1+WP(j),w=_;default:if(x<1){if(D==123)--x;else if(D==125&&x++==0&&cur()==125)continue}switch(j+=Det(D),D*x){case 38:I=p>0?1:(j+="\f",-1);break;case 44:c[h++]=(WP(j)-1)*I,I=1;break;case 64:x9()===45&&(j+=hUe(HL())),b=x9(),p=m=WP(A=j+=fur(dUe())),D++;break;case 45:_===45&&WP(j)==2&&(x=0)}}return o}function yNn(n,e,t,i,r,o,l,c,d,h,p){for(var m=r-1,b=r===0?o:[""],w=Szt(b),_=0,x=0,T=0;_0?b[I]+" "+D:zf(D,/&\f/g,b[I])))&&(d[T++]=A);return Aet(n,e,t,r===0?_zt:c,d,h,p)}function gur(n,e,t){return Aet(n,e,t,qli,Det(lur()),n6e(n,2,-2),0)}function _Nn(n,e,t,i){return Aet(n,e,t,Czt,n6e(n,0,i),n6e(n,i+1,-1),i)}function Gpe(n,e){for(var t="",i=Szt(n),r=0;r6)switch(G2(n,e+1)){case 109:if(G2(n,e+4)!==45)break;case 102:return zf(n,/(.+:)(.+)-([^]+)/,"$1"+Vf+"$2-$3$1"+Kqe+(G2(n,e+3)==108?"$3":"$2-$3"))+n;case 115:return~SOt(n,"stretch")?Jli(zf(n,"stretch","fill-available"),e)+n:n}break;case 4949:if(G2(n,e+1)!==115)break;case 6444:switch(G2(n,WP(n)-3-(~SOt(n,"!important")&&10))){case 107:return zf(n,":",":"+Vf)+n;case 101:return zf(n,/(.+:)([^;!]+)(;|!.+)?/,"$1"+Vf+(G2(n,14)===45?"inline-":"")+"box$3$1"+Vf+"$2$3$1"+cS+"$2box$3")+n}break;case 5936:switch(G2(n,e+11)){case 114:return Vf+n+cS+zf(n,/[svh]\w+-[tblr]{2}/,"tb")+n;case 108:return Vf+n+cS+zf(n,/[svh]\w+-[tblr]{2}/,"tb-rl")+n;case 45:return Vf+n+cS+zf(n,/[svh]\w+-[tblr]{2}/,"lr")+n}return Vf+n+cS+n+n}return n}var xur=function(e,t,i,r){if(e.length>-1&&!e.return)switch(e.type){case Czt:e.return=Jli(e.value,e.length);break;case Gli:return Gpe([Rxe(e,{value:zf(e.value,"@","@"+Vf)})],r);case _zt:if(e.length)return aur(e.props,function(o){switch(our(o,/(::plac\w+|:read-\w+)/)){case":read-only":case":read-write":return Gpe([Rxe(e,{props:[zf(o,/:(read-\w+)/,":"+Kqe+"$1")]})],r);case"::placeholder":return Gpe([Rxe(e,{props:[zf(o,/:(plac\w+)/,":"+Vf+"input-$1")]}),Rxe(e,{props:[zf(o,/:(plac\w+)/,":"+Kqe+"$1")]}),Rxe(e,{props:[zf(o,/:(plac\w+)/,cS+"input-$1")]})],r)}return""})}},Eur=[xur],kur=function(e){var t=e.key;if(t==="css"){var i=document.querySelectorAll("style[data-emotion]:not([data-s])");Array.prototype.forEach.call(i,function(x){var T=x.getAttribute("data-emotion");T.indexOf(" ")!==-1&&(document.head.appendChild(x),x.setAttribute("data-s",""))})}var r=e.stylisPlugins||Eur,o={},l,c=[];l=e.container||document.head,Array.prototype.forEach.call(document.querySelectorAll('style[data-emotion^="'+t+' "]'),function(x){for(var T=x.getAttribute("data-emotion").split(" "),I=1;I=4;++i,r-=4)t=n.charCodeAt(i)&255|(n.charCodeAt(++i)&255)<<8|(n.charCodeAt(++i)&255)<<16|(n.charCodeAt(++i)&255)<<24,t=(t&65535)*1540483477+((t>>>16)*59797<<16),t^=t>>>24,e=(t&65535)*1540483477+((t>>>16)*59797<<16)^(e&65535)*1540483477+((e>>>16)*59797<<16);switch(r){case 3:e^=(n.charCodeAt(i+2)&255)<<16;case 2:e^=(n.charCodeAt(i+1)&255)<<8;case 1:e^=n.charCodeAt(i)&255,e=(e&65535)*1540483477+((e>>>16)*59797<<16)}return e^=e>>>13,e=(e&65535)*1540483477+((e>>>16)*59797<<16),((e^e>>>15)>>>0).toString(36)}var Our={animationIterationCount:1,aspectRatio:1,borderImageOutset:1,borderImageSlice:1,borderImageWidth:1,boxFlex:1,boxFlexGroup:1,boxOrdinalGroup:1,columnCount:1,columns:1,flex:1,flexGrow:1,flexPositive:1,flexShrink:1,flexNegative:1,flexOrder:1,gridRow:1,gridRowEnd:1,gridRowSpan:1,gridRowStart:1,gridColumn:1,gridColumnEnd:1,gridColumnSpan:1,gridColumnStart:1,msGridRow:1,msGridRowSpan:1,msGridColumn:1,msGridColumnSpan:1,fontWeight:1,lineHeight:1,opacity:1,order:1,orphans:1,scale:1,tabSize:1,widows:1,zIndex:1,zoom:1,WebkitLineClamp:1,fillOpacity:1,floodOpacity:1,stopOpacity:1,strokeDasharray:1,strokeDashoffset:1,strokeMiterlimit:1,strokeOpacity:1,strokeWidth:1},Nur=/[A-Z]|^ms/g,Pur=/_EMO_([^_]+?)_([^]*?)_EMO_/g,tci=function(e){return e.charCodeAt(1)===45},kNn=function(e){return e!=null&&typeof e!="boolean"},xEt=Qli(function(n){return tci(n)?n:n.replace(Nur,"-$&").toLowerCase()}),TNn=function(e,t){switch(e){case"animation":case"animationName":if(typeof t=="string")return t.replace(Pur,function(i,r,o){return VP={name:r,styles:o,next:VP},r})}return Our[e]!==1&&!tci(e)&&typeof t=="number"&&t!==0?t+"px":t};function r6e(n,e,t){if(t==null)return"";var i=t;if(i.__emotion_styles!==void 0)return i;switch(typeof t){case"boolean":return"";case"object":{var r=t;if(r.anim===1)return VP={name:r.name,styles:r.styles,next:VP},r.name;var o=t;if(o.styles!==void 0){var l=o.next;if(l!==void 0)for(;l!==void 0;)VP={name:l.name,styles:l.styles,next:VP},l=l.next;var c=o.styles+";";return c}return Fur(n,e,t)}case"function":{if(n!==void 0){var d=VP,h=t(n);return VP=d,r6e(n,e,h)}break}}var p=t;if(e==null)return p;var m=e[p];return m!==void 0?m:p}function Fur(n,e,t){var i="";if(Array.isArray(t))for(var r=0;r96?Uur:qur},RNn=function(e,t,i){var r;if(t){var o=t.shouldForwardProp;r=e.__emotion_forwardProp&&o?function(l){return e.__emotion_forwardProp(l)&&o(l)}:o}return typeof r!="function"&&i&&(r=e.__emotion_forwardProp),r},Gur=function(e){var t=e.cache,i=e.serialized,r=e.isStringTag;return xzt(t,i,r),ici(function(){return Ezt(t,i,r)}),null},Kur=function n(e,t){var i=e.__emotion_real===e,r=i&&e.__emotion_base||e,o,l;t!==void 0&&(o=t.label,l=t.target);var c=RNn(e,t,i),d=c||ANn(r),h=!d("as");return function(){var p=arguments,m=i&&e.__emotion_styles!==void 0?e.__emotion_styles.slice(0):[];if(o!==void 0&&m.push("label:"+o+";"),p[0]==null||p[0].raw===void 0)m.push.apply(m,p);else{var b=p[0];m.push(b[0]);for(var w=p.length,_=1;_e(Zur(r)?t:r):e;return k.jsx($ur,{styles:i})}function oci(n,e){return TOt(n,e)}function Xur(n,e){Array.isArray(n.__emotion_styles)&&(n.__emotion_styles=e(n.__emotion_styles))}const MNn=[];function pK(n){return MNn[0]=n,c5e(MNn)}const Qur=n=>{const e=Object.keys(n).map(t=>({key:t,val:n[t]}))||[];return e.sort((t,i)=>t.val-i.val),e.reduce((t,i)=>({...t,[i.key]:i.val}),{})};function Jur(n){const{values:e={xs:0,sm:600,md:900,lg:1200,xl:1536},unit:t="px",step:i=5,...r}=n,o=Qur(e),l=Object.keys(o);function c(b){return`@media (min-width:${typeof e[b]=="number"?e[b]:b}${t})`}function d(b){return`@media (max-width:${(typeof e[b]=="number"?e[b]:b)-i/100}${t})`}function h(b,w){const _=l.indexOf(w);return`@media (min-width:${typeof e[b]=="number"?e[b]:b}${t}) and (max-width:${(_!==-1&&typeof e[l[_]]=="number"?e[l[_]]:w)-i/100}${t})`}function p(b){return l.indexOf(b)+1(i.length===0?[1]:i).map(o=>{const l=e(o);return typeof l=="number"?`${l}px`:l}).join(" ");return t.mui=!0,t}function tdr(n,e){const t=this;if(t.vars){if(!t.colorSchemes?.[n]||typeof t.getColorSchemeSelector!="function")return{};let i=t.getColorSchemeSelector(n);return i==="&"?e:((i.includes("data-")||i.includes("."))&&(i=`*:where(${i.replace(/\s*&$/,"")}) &`),{[i]:e})}return t.palette.mode===n?e:{}}function d5e(n={},...e){const{breakpoints:t={},palette:i={},spacing:r,shape:o={},...l}=n,c=Jur(t),d=aci(r);let h=F_({breakpoints:c,direction:"ltr",components:{},palette:{mode:"light",...i},spacing:d,shape:{...edr,...o}},l);return h=lcr(h),h.applyStyles=tdr,h=e.reduce((p,m)=>F_(p,m),h),h.unstable_sxConfig={...a5e,...l?.unstable_sxConfig},h.unstable_sx=function(m){return XK({sx:m,theme:this})},h}function ndr(n){return Object.keys(n).length===0}function Ret(n=null){const e=L.useContext(u5e);return!e||ndr(e)?n:e}const idr=d5e();function ooe(n=idr){return Ret(n)}function EEt(n){const e=pK(n);return n!==e&&e.styles?(e.styles.match(/^@layer\s+[^{]*$/)||(e.styles=`@layer global{${e.styles}}`),e):n}function lci({styles:n,themeId:e,defaultTheme:t={}}){const i=ooe(t),r=e&&i[e]||i;let o=typeof n=="function"?n(r):n;return r.modularCssLayers&&(Array.isArray(o)?o=o.map(l=>EEt(typeof l=="function"?l(r):l)):o=EEt(o)),k.jsx(sci,{styles:o})}const ONn=n=>n,rdr=()=>{let n=ONn;return{configure(e){n=e},generate(e){return n(e)},reset(){n=ONn}}},Lzt=rdr();function cci(n){var e,t,i="";if(typeof n=="string"||typeof n=="number")i+=n;else if(typeof n=="object")if(Array.isArray(n)){var r=n.length;for(e=0;ec!=="theme"&&c!=="sx"&&c!=="as"})(XK);return L.forwardRef(function(d,h){const p=ooe(t),{className:m,component:b="div",...w}=Let(d);return k.jsx(o,{as:b,ref:h,className:_i(m,r?r(i):i),theme:e&&p[e]||p,...w})})}const sdr={active:"active",checked:"checked",completed:"completed",disabled:"disabled",error:"error",expanded:"expanded",focused:"focused",focusVisible:"focusVisible",open:"open",readOnly:"readOnly",required:"required",selected:"selected"};function Po(n,e,t="Mui"){const i=sdr[e];return i?`${t}-${i}`:`${Lzt.generate(n)}-${e}`}function Fo(n,e,t="Mui"){const i={};return e.forEach(r=>{i[r]=Po(n,r,t)}),i}const odr=Fo("MuiBox",["root"]),xBe=uci({defaultClassName:odr.root,generateClassName:Lzt.generate});function dci(n){const{variants:e,...t}=n,i={variants:e,style:pK(t),isProcessed:!0};return i.style===t||e&&e.forEach(r=>{typeof r.style!="function"&&(r.style=pK(r.style))}),i}const adr=d5e();function kEt(n){return n!=="ownerState"&&n!=="theme"&&n!=="sx"&&n!=="as"}function Vne(n,e){return e&&n&&typeof n=="object"&&n.styles&&!n.styles.startsWith("@layer")&&(n.styles=`@layer ${e}{${String(n.styles)}}`),n}function ldr(n){return n?(e,t)=>t[n]:null}function cdr(n,e,t){n.theme=ddr(n.theme)?t:n.theme[e]||n.theme}function pUe(n,e,t){const i=typeof e=="function"?e(n):e;if(Array.isArray(i))return i.flatMap(r=>pUe(n,r,t));if(Array.isArray(i?.variants)){let r;if(i.isProcessed)r=t?Vne(i.style,t):i.style;else{const{variants:o,...l}=i;r=t?Vne(pK(l),t):l}return hci(n,i.variants,[r],t)}return i?.isProcessed?t?Vne(pK(i.style),t):i.style:t?Vne(pK(i),t):i}function hci(n,e,t=[],i=void 0){let r;e:for(let o=0;o{Xur(c,F=>F.filter(j=>j!==XK));const{name:h,slot:p,skipVariantsResolver:m,skipSx:b,overridesResolver:w=ldr(fdr(p)),..._}=d,x=h&&h.startsWith("Mui")||p?"components":"custom",T=m!==void 0?m:p&&p!=="Root"&&p!=="root"||!1,I=b||!1;let D=kEt;p==="Root"||p==="root"?D=i:p?D=r:hdr(c)&&(D=void 0);const A=oci(c,{shouldForwardProp:D,label:udr(),..._}),M=F=>{if(F.__emotion_real===F)return F;if(typeof F=="function")return function(W){return pUe(W,F,W.theme.modularCssLayers?x:void 0)};if(ZP(F)){const j=dci(F);return function(U){return j.variants?pUe(U,j,U.theme.modularCssLayers?x:void 0):U.theme.modularCssLayers?Vne(j.style,x):j.style}}return F},O=(...F)=>{const j=[],W=F.map(M),U=[];if(j.push(o),h&&w&&U.push(function(ee){const ie=ee.theme.components?.[h]?.styleOverrides;if(!ie)return null;const se={};for(const ue in ie)se[ue]=pUe(ee,ie[ue],ee.theme.modularCssLayers?"theme":void 0);return w(ee,se)}),h&&!T&&U.push(function(ee){const ie=ee.theme?.components?.[h]?.variants;return ie?hci(ee,ie,[],ee.theme.modularCssLayers?"theme":void 0):null}),I||U.push(XK),Array.isArray(W[0])){const G=W.shift(),ee=new Array(j.length).fill(""),Q=new Array(U.length).fill("");let ie;ie=[...ee,...G,...Q],ie.raw=[...ee,...G.raw,...Q],j.unshift(ie)}const Z=[...j,...W,...U],te=A(...Z);return c.muiName&&(te.muiName=c.muiName),te};return A.withConfig&&(O.withConfig=A.withConfig),O}}function udr(n,e){return void 0}function ddr(n){for(const e in n)return!1;return!0}function hdr(n){return typeof n=="string"&&n.charCodeAt(0)>96}function fdr(n){return n&&n.charAt(0).toLowerCase()+n.slice(1)}const aoe=fci();function s6e(n,e,t=!1){const i={...e};for(const r in n)if(Object.prototype.hasOwnProperty.call(n,r)){const o=r;if(o==="components"||o==="slots")i[o]={...n[o],...i[o]};else if(o==="componentsProps"||o==="slotProps"){const l=n[o],c=e[o];if(!c)i[o]=l||{};else if(!l)i[o]=c;else{i[o]={...c};for(const d in l)if(Object.prototype.hasOwnProperty.call(l,d)){const h=d;i[o][h]=s6e(l[h],c[h],t)}}}else o==="className"&&t&&e.className?i.className=_i(n?.className,e?.className):o==="style"&&t&&e.style?i.style={...n?.style,...e?.style}:i[o]===void 0&&(i[o]=n[o])}return i}function pci(n){const{theme:e,name:t,props:i}=n;return!e||!e.components||!e.components[t]||!e.components[t].defaultProps?i:s6e(e.components[t].defaultProps,i)}function Dzt({props:n,name:e,defaultTheme:t,themeId:i}){let r=ooe(t);return i&&(r=r[i]||r),pci({theme:r,name:e,props:n})}const IS=typeof window<"u"?L.useLayoutEffect:L.useEffect;function pdr(n,e,t,i,r){const[o,l]=L.useState(()=>r&&t?t(n).matches:i?i(n).matches:e);return IS(()=>{if(!t)return;const c=t(n),d=()=>{l(c.matches)};return d(),c.addEventListener("change",d),()=>{c.removeEventListener("change",d)}},[n,t]),o}const gdr={...z9},gci=gdr.useSyncExternalStore;function mdr(n,e,t,i,r){const o=L.useCallback(()=>e,[e]),l=L.useMemo(()=>{if(r&&t)return()=>t(n).matches;if(i!==null){const{matches:p}=i(n);return()=>p}return o},[o,n,i,r,t]),[c,d]=L.useMemo(()=>{if(t===null)return[o,()=>()=>{}];const p=t(n);return[()=>p.matches,m=>(p.addEventListener("change",m),()=>{p.removeEventListener("change",m)})]},[o,t,n]);return gci(d,c,l)}function mci(n={}){const{themeId:e}=n;return function(i,r={}){let o=Ret();o&&e&&(o=o[e]||o);const l=typeof window<"u"&&typeof window.matchMedia<"u",{defaultMatches:c=!1,matchMedia:d=l?window.matchMedia:null,ssrMatchMedia:h=null,noSsr:p=!1}=pci({name:"MuiUseMediaQuery",props:r,theme:o});let m=typeof i=="function"?i(o):i;return m=m.replace(/^@media( ?)/m,""),m.includes("print")&&console.warn(["MUI: You have provided a `print` query to the `useMediaQuery` hook.","Using the print media query to modify print styles can lead to unexpected results.","Consider using the `displayPrint` field in the `sx` prop instead.","More information about `displayPrint` on our docs: https://mui.com/system/display/#display-in-print."].join(` +`)),(gci!==void 0?mdr:pdr)(m,c,d,h,p)}}mci();function ufe(n,e=Number.MIN_SAFE_INTEGER,t=Number.MAX_SAFE_INTEGER){return Math.max(e,Math.min(n,t))}function Izt(n,e=0,t=1){return ufe(n,e,t)}function bdr(n){n=n.slice(1);const e=new RegExp(`.{1,${n.length>=6?2:1}}`,"g");let t=n.match(e);return t&&t[0].length===1&&(t=t.map(i=>i+i)),t?`rgb${t.length===4?"a":""}(${t.map((i,r)=>r<3?parseInt(i,16):Math.round(parseInt(i,16)/255*1e3)/1e3).join(", ")})`:""}function QK(n){if(n.type)return n;if(n.charAt(0)==="#")return QK(bdr(n));const e=n.indexOf("("),t=n.substring(0,e);if(!["rgb","rgba","hsl","hsla","color"].includes(t))throw new Error(mW(9,n));let i=n.substring(e+1,n.length-1),r;if(t==="color"){if(i=i.split(" "),r=i.shift(),i.length===4&&i[3].charAt(0)==="/"&&(i[3]=i[3].slice(1)),!["srgb","display-p3","a98-rgb","prophoto-rgb","rec-2020"].includes(r))throw new Error(mW(10,r))}else i=i.split(",");return i=i.map(o=>parseFloat(o)),{type:t,values:i,colorSpace:r}}const vdr=n=>{const e=QK(n);return e.values.slice(0,3).map((t,i)=>e.type.includes("hsl")&&i!==0?`${t}%`:t).join(" ")},ske=(n,e)=>{try{return vdr(n)}catch{return n}};function Met(n){const{type:e,colorSpace:t}=n;let{values:i}=n;return e.includes("rgb")?i=i.map((r,o)=>o<3?parseInt(r,10):r):e.includes("hsl")&&(i[1]=`${i[1]}%`,i[2]=`${i[2]}%`),e.includes("color")?i=`${t} ${i.join(" ")}`:i=`${i.join(", ")}`,`${e}(${i})`}function bci(n){n=QK(n);const{values:e}=n,t=e[0],i=e[1]/100,r=e[2]/100,o=i*Math.min(r,1-r),l=(h,p=(h+t/30)%12)=>r-o*Math.max(Math.min(p-3,9-p,1),-1);let c="rgb";const d=[Math.round(l(0)*255),Math.round(l(8)*255),Math.round(l(4)*255)];return n.type==="hsla"&&(c+="a",d.push(e[3])),Met({type:c,values:d})}function LOt(n){n=QK(n);let e=n.type==="hsl"||n.type==="hsla"?QK(bci(n)).values:n.values;return e=e.map(t=>(n.type!=="color"&&(t/=255),t<=.03928?t/12.92:((t+.055)/1.055)**2.4)),Number((.2126*e[0]+.7152*e[1]+.0722*e[2]).toFixed(3))}function wdr(n,e){const t=LOt(n),i=LOt(e);return(Math.max(t,i)+.05)/(Math.min(t,i)+.05)}function Wa(n,e){return n=QK(n),e=Izt(e),(n.type==="rgb"||n.type==="hsl")&&(n.type+="a"),n.type==="color"?n.values[3]=`/${e}`:n.values[3]=e,Met(n)}function dte(n,e,t){try{return Wa(n,e)}catch{return n}}function Oet(n,e){if(n=QK(n),e=Izt(e),n.type.includes("hsl"))n.values[2]*=1-e;else if(n.type.includes("rgb")||n.type.includes("color"))for(let t=0;t<3;t+=1)n.values[t]*=1-e;return Met(n)}function Up(n,e,t){try{return Oet(n,e)}catch{return n}}function Net(n,e){if(n=QK(n),e=Izt(e),n.type.includes("hsl"))n.values[2]+=(100-n.values[2])*e;else if(n.type.includes("rgb"))for(let t=0;t<3;t+=1)n.values[t]+=(255-n.values[t])*e;else if(n.type.includes("color"))for(let t=0;t<3;t+=1)n.values[t]+=(1-n.values[t])*e;return Met(n)}function qp(n,e,t){try{return Net(n,e)}catch{return n}}function o6e(n,e=.15){return LOt(n)>.5?Oet(n,e):Net(n,e)}function EBe(n,e,t){try{return o6e(n,e)}catch{return n}}const vci=L.createContext(null);function Azt(){return L.useContext(vci)}const ydr=typeof Symbol=="function"&&Symbol.for,_dr=ydr?Symbol.for("mui.nested"):"__THEME_NESTED__";function Cdr(n,e){return typeof e=="function"?e(n):{...n,...e}}function Sdr(n){const{children:e,theme:t}=n,i=Azt(),r=L.useMemo(()=>{const o=i===null?{...t}:Cdr(i,t);return o!=null&&(o[_dr]=i!==null),o},[t,i]);return k.jsx(vci.Provider,{value:r,children:e})}const wci=L.createContext();function xdr({value:n,...e}){return k.jsx(wci.Provider,{value:n??!0,...e})}const OY=()=>L.useContext(wci)??!1,yci=L.createContext(void 0);function Edr({value:n,children:e}){return k.jsx(yci.Provider,{value:n,children:e})}function kdr(n){const{theme:e,name:t,props:i}=n;if(!e||!e.components||!e.components[t])return i;const r=e.components[t];return r.defaultProps?s6e(r.defaultProps,i,e.components.mergeClassNameAndStyle):!r.styleOverrides&&!r.variants?s6e(r,i,e.components.mergeClassNameAndStyle):i}function Tdr({props:n,name:e}){const t=L.useContext(yci);return kdr({props:n,name:e,theme:{components:t}})}let NNn=0;function Ldr(n){const[e,t]=L.useState(n),i=n||e;return L.useEffect(()=>{e==null&&(NNn+=1,t(`mui-${NNn}`))},[e]),i}const Ddr={...z9},PNn=Ddr.useId;function KW(n){if(PNn!==void 0){const e=PNn();return n??e}return Ldr(n)}function Idr(n){const e=Ret(),t=KW()||"",{modularCssLayers:i}=n;let r="mui.global, mui.components, mui.theme, mui.custom, mui.sx";return!i||e!==null?r="":typeof i=="string"?r=i.replace(/mui(?!\.)/g,r):r=`@layer ${r};`,IS(()=>{const o=document.querySelector("head");if(!o)return;const l=o.firstChild;if(r){if(l&&l.hasAttribute?.("data-mui-layer-order")&&l.getAttribute("data-mui-layer-order")===t)return;const c=document.createElement("style");c.setAttribute("data-mui-layer-order",t),c.textContent=r,o.prepend(c)}else o.querySelector(`style[data-mui-layer-order="${t}"]`)?.remove()},[r,t]),r?k.jsx(lci,{styles:r}):null}const FNn={};function jNn(n,e,t,i=!1){return L.useMemo(()=>{const r=n&&e[n]||e;if(typeof t=="function"){const o=t(r),l=n?{...e,[n]:o}:o;return i?()=>l:l}return n?{...e,[n]:t}:{...e,...t}},[n,e,t,i])}function _ci(n){const{children:e,theme:t,themeId:i}=n,r=Ret(FNn),o=Azt()||FNn,l=jNn(i,r,t),c=jNn(i,o,t,!0),d=(i?l[i]:l).direction==="rtl",h=Idr(l);return k.jsx(Sdr,{theme:c,children:k.jsx(u5e.Provider,{value:l,children:k.jsx(xdr,{value:d,children:k.jsxs(Edr,{value:i?l[i].components:l.components,children:[h,e]})})})})}const HNn={theme:void 0};function Adr(n){let e,t;return function(r){let o=e;return(o===void 0||r.theme!==t)&&(HNn.theme=r.theme,o=dci(n(HNn)),e=o,t=r.theme),o}}const Rzt="mode",Mzt="color-scheme",Rdr="data-color-scheme";function Mdr(n){const{defaultMode:e="system",defaultLightColorScheme:t="light",defaultDarkColorScheme:i="dark",modeStorageKey:r=Rzt,colorSchemeStorageKey:o=Mzt,attribute:l=Rdr,colorSchemeNode:c="document.documentElement",nonce:d}=n||{};let h="",p=l;if(l==="class"&&(p=".%s"),l==="data"&&(p="[data-%s]"),p.startsWith(".")){const b=p.substring(1);h+=`${c}.classList.remove('${b}'.replace('%s', light), '${b}'.replace('%s', dark)); ${c}.classList.add('${b}'.replace('%s', colorScheme));`}const m=p.match(/\[([^[\]]+)\]/);if(m){const[b,w]=m[1].split("=");w||(h+=`${c}.removeAttribute('${b}'.replace('%s', light)); ${c}.removeAttribute('${b}'.replace('%s', dark));`),h+=` ${c}.setAttribute('${b}'.replace('%s', colorScheme), ${w?`${w}.replace('%s', colorScheme)`:'""'});`}else p!==".%s"&&(h+=`${c}.setAttribute('${p}', colorScheme);`);return k.jsx("script",{suppressHydrationWarning:!0,nonce:typeof window>"u"?d:"",dangerouslySetInnerHTML:{__html:`(function() { @@ -27,24 +27,24 @@ try { if (colorScheme) { ${h} } -} catch(e){}})();`}},"mui-color-scheme-init")}function Ldr(){}const Ddr=({key:n,storageWindow:e})=>(!e&&typeof window<"u"&&(e=window),{get(t){if(typeof window>"u")return;if(!e)return t;let i;try{i=e.localStorage.getItem(n)}catch{}return i||t},set:t=>{if(e)try{e.localStorage.setItem(n,t)}catch{}},subscribe:t=>{if(!e)return Ldr;const i=r=>{const o=r.newValue;r.key===n&&t(o)};return e.addEventListener("storage",i),()=>{e.removeEventListener("storage",i)}}});function TEt(){}function HNn(n){if(typeof window<"u"&&typeof window.matchMedia=="function"&&n==="system")return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function wci(n,e){if(n.mode==="light"||n.mode==="system"&&n.systemMode==="light")return e("light");if(n.mode==="dark"||n.mode==="system"&&n.systemMode==="dark")return e("dark")}function Idr(n){return wci(n,e=>{if(e==="light")return n.lightColorScheme;if(e==="dark")return n.darkColorScheme})}function Adr(n){const{defaultMode:e="light",defaultLightColorScheme:t,defaultDarkColorScheme:i,supportedColorSchemes:r=[],modeStorageKey:o=Ozt,colorSchemeStorageKey:l=Nzt,storageWindow:c=typeof window>"u"?void 0:window,storageManager:d=Ddr,noSsr:h=!1}=n,p=r.join(","),m=r.length>1,b=D.useMemo(()=>d?.({key:o,storageWindow:c}),[d,o,c]),w=D.useMemo(()=>d?.({key:`${l}-light`,storageWindow:c}),[d,l,c]),_=D.useMemo(()=>d?.({key:`${l}-dark`,storageWindow:c}),[d,l,c]),[x,T]=D.useState(()=>{const W=b?.get(e)||e,q=w?.get(t)||t,Z=_?.get(i)||i;return{mode:W,systemMode:HNn(W),lightColorScheme:q,darkColorScheme:Z}}),[I,L]=D.useState(h||!m);D.useEffect(()=>{L(!0)},[]);const A=Idr(x),M=D.useCallback(W=>{T(q=>{if(W===q.mode)return q;const Z=W??e;return b?.set(Z),{...q,mode:Z,systemMode:HNn(Z)}})},[b,e]),O=D.useCallback(W=>{W?typeof W=="string"?W&&!p.includes(W)?console.error(`\`${W}\` does not exist in \`theme.colorSchemes\`.`):T(q=>{const Z={...q};return wci(q,ee=>{ee==="light"&&(w?.set(W),Z.lightColorScheme=W),ee==="dark"&&(_?.set(W),Z.darkColorScheme=W)}),Z}):T(q=>{const Z={...q},ee=W.light===null?t:W.light,G=W.dark===null?i:W.dark;return ee&&(p.includes(ee)?(Z.lightColorScheme=ee,w?.set(ee)):console.error(`\`${ee}\` does not exist in \`theme.colorSchemes\`.`)),G&&(p.includes(G)?(Z.darkColorScheme=G,_?.set(G)):console.error(`\`${G}\` does not exist in \`theme.colorSchemes\`.`)),Z}):T(q=>(w?.set(t),_?.set(i),{...q,lightColorScheme:t,darkColorScheme:i}))},[p,w,_,t,i]),F=D.useCallback(W=>{x.mode==="system"&&T(q=>{const Z=W?.matches?"dark":"light";return q.systemMode===Z?q:{...q,systemMode:Z}})},[x.mode]),j=D.useRef(F);return j.current=F,D.useEffect(()=>{if(typeof window.matchMedia!="function"||!m)return;const W=(...Z)=>j.current(...Z),q=window.matchMedia("(prefers-color-scheme: dark)");return q.addListener(W),W(q),()=>{q.removeListener(W)}},[m]),D.useEffect(()=>{if(m){const W=b?.subscribe(ee=>{(!ee||["light","dark","system"].includes(ee))&&M(ee||e)})||TEt,q=w?.subscribe(ee=>{(!ee||p.match(ee))&&O({light:ee})})||TEt,Z=_?.subscribe(ee=>{(!ee||p.match(ee))&&O({dark:ee})})||TEt;return()=>{W(),q(),Z()}}},[O,M,p,e,c,m,b,w,_]),{...x,mode:I?x.mode:void 0,systemMode:I?x.systemMode:void 0,colorScheme:I?A:void 0,setMode:M,setColorScheme:O}}const Rdr="*{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}";function Mdr(n){const{themeId:e,theme:t={},modeStorageKey:i=Ozt,colorSchemeStorageKey:r=Nzt,disableTransitionOnChange:o=!1,defaultColorScheme:l,resolveTheme:c}=n,d={allColorSchemes:[],colorScheme:void 0,darkColorScheme:void 0,lightColorScheme:void 0,mode:void 0,setColorScheme:()=>{},setMode:()=>{},systemMode:void 0},h=D.createContext(void 0),p=()=>D.useContext(h)||d,m={},b={};function w(I){const{children:L,theme:A,modeStorageKey:M=i,colorSchemeStorageKey:O=r,disableTransitionOnChange:F=o,storageManager:j,storageWindow:W=typeof window>"u"?void 0:window,documentNode:q=typeof document>"u"?void 0:document,colorSchemeNode:Z=typeof document>"u"?void 0:document.documentElement,disableNestedContext:ee=!1,disableStyleSheetGeneration:G=!1,defaultMode:te="system",forceThemeRerender:Q=!1,noSsr:ie}=I,se=D.useRef(!1),de=Mzt(),ne=D.useContext(h),we=!!ne&&!ee,ue=D.useMemo(()=>A||(typeof t=="function"?t():t),[A]),ce=ue[e],ye=ce||ue,{colorSchemes:he=m,components:pe=b,cssVarPrefix:me}=ye,be=Object.keys(he).filter(Ye=>!!he[Ye]).join(","),xe=D.useMemo(()=>be.split(","),[be]),Te=typeof l=="string"?l:l.light,Ge=typeof l=="string"?l:l.dark,tt=he[Te]&&he[Ge]?te:he[ye.defaultColorScheme]?.palette?.mode||ye.palette?.mode,{mode:Ue,setMode:Me,systemMode:He,lightColorScheme:at,darkColorScheme:rt,colorScheme:Be,setColorScheme:lt}=Adr({supportedColorSchemes:xe,defaultLightColorScheme:Te,defaultDarkColorScheme:Ge,modeStorageKey:M,colorSchemeStorageKey:O,defaultMode:tt,storageManager:j,storageWindow:W,noSsr:ie});let ct=Ue,ze=Be;we&&(ct=ne.mode,ze=ne.colorScheme);let Ke=ze||ye.defaultColorScheme;ye.vars&&!Q&&(Ke=ye.defaultColorScheme);const $e=D.useMemo(()=>{const Ye=ye.generateThemeVars?.()||ye.vars,wt={...ye,components:pe,colorSchemes:he,cssVarPrefix:me,vars:Ye};if(typeof wt.generateSpacing=="function"&&(wt.spacing=wt.generateSpacing()),Ke){const zt=he[Ke];zt&&typeof zt=="object"&&Object.keys(zt).forEach(mn=>{zt[mn]&&typeof zt[mn]=="object"?wt[mn]={...wt[mn],...zt[mn]}:wt[mn]=zt[mn]})}return c?c(wt):wt},[ye,Ke,pe,he,me]),nt=ye.colorSchemeSelector;IS(()=>{if(ze&&Z&&nt&&nt!=="media"){const Ye=nt;let wt=nt;if(Ye==="class"&&(wt=".%s"),Ye==="data"&&(wt="[data-%s]"),Ye?.startsWith("data-")&&!Ye.includes("%s")&&(wt=`[${Ye}="%s"]`),wt.startsWith("."))Z.classList.remove(...xe.map(zt=>wt.substring(1).replace("%s",zt))),Z.classList.add(wt.substring(1).replace("%s",ze));else{const zt=wt.replace("%s",ze).match(/\[([^\]]+)\]/);if(zt){const[mn,xn]=zt[1].split("=");xn||xe.forEach(hn=>{Z.removeAttribute(mn.replace(ze,hn))}),Z.setAttribute(mn,xn?xn.replace(/"|'/g,""):"")}else Z.setAttribute(wt,ze)}}},[ze,nt,Z,xe]),D.useEffect(()=>{let Ye;if(F&&se.current&&q){const wt=q.createElement("style");wt.appendChild(q.createTextNode(Rdr)),q.head.appendChild(wt),window.getComputedStyle(q.body),Ye=setTimeout(()=>{q.head.removeChild(wt)},1)}return()=>{clearTimeout(Ye)}},[ze,F,q]),D.useEffect(()=>(se.current=!0,()=>{se.current=!1}),[]);const vt=D.useMemo(()=>({allColorSchemes:xe,colorScheme:ze,darkColorScheme:rt,lightColorScheme:at,mode:ct,setColorScheme:lt,setMode:Me,systemMode:He}),[xe,ze,rt,at,ct,lt,Me,He,$e.colorSchemeSelector]);let Pt=!0;(G||ye.cssVariables===!1||we&&de?.cssVarPrefix===me)&&(Pt=!1);const Ct=k.jsxs(D.Fragment,{children:[k.jsx(vci,{themeId:ce?e:void 0,theme:$e,children:L}),Pt&&k.jsx(nci,{styles:$e.generateStyleSheets?.()||[]})]});return we?Ct:k.jsx(h.Provider,{value:vt,children:Ct})}const _=typeof l=="string"?l:l.light,x=typeof l=="string"?l:l.dark;return{CssVarsProvider:w,useColorScheme:p,getInitColorSchemeScript:I=>Tdr({colorSchemeStorageKey:r,defaultLightColorScheme:_,defaultDarkColorScheme:x,modeStorageKey:i,...I})}}function Odr(n=""){function e(...i){if(!i.length)return"";const r=i[0];return typeof r=="string"&&!r.match(/(#|\(|\)|(-?(\d*\.)?\d+)(px|em|%|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc))|^(-?(\d*\.)?\d+)$|(\d+ \d+ \d+)/)?`, var(--${n?`${n}-`:""}${r}${e(...i.slice(1))})`:`, ${r}`}return(i,...r)=>`var(--${n?`${n}-`:""}${i}${e(...r)})`}const BNn=(n,e,t,i=[])=>{let r=n;e.forEach((o,l)=>{l===e.length-1?Array.isArray(r)?r[Number(o)]=t:r&&typeof r=="object"&&(r[o]=t):r&&typeof r=="object"&&(r[o]||(r[o]=i.includes(o)?[]:{}),r=r[o])})},Ndr=(n,e,t)=>{function i(r,o=[],l=[]){Object.entries(r).forEach(([c,d])=>{(!t||t&&!t([...o,c]))&&d!=null&&(typeof d=="object"&&Object.keys(d).length>0?i(d,[...o,c],Array.isArray(d)?[...l,c]:l):e([...o,c],d,l))})}i(n)},Pdr=(n,e)=>typeof e=="number"?["lineHeight","fontWeight","opacity","zIndex"].some(i=>n.includes(i))||n[n.length-1].toLowerCase().includes("opacity")?e:`${e}px`:e;function LEt(n,e){const{prefix:t,shouldSkipGeneratingVar:i}=e||{},r={},o={},l={};return Ndr(n,(c,d,h)=>{if((typeof d=="string"||typeof d=="number")&&(!i||!i(c,d))){const p=`--${t?`${t}-`:""}${c.join("-")}`,m=Pdr(c,d);Object.assign(r,{[p]:m}),BNn(o,c,`var(${p})`,h),BNn(l,c,`var(${p}, ${m})`,h)}},c=>c[0]==="vars"),{css:r,vars:o,varsWithDefaults:l}}function Fdr(n,e={}){const{getSelector:t=I,disableCssColorScheme:i,colorSchemeSelector:r,enableContrastVars:o}=e,{colorSchemes:l={},components:c,defaultColorScheme:d="light",...h}=n,{vars:p,css:m,varsWithDefaults:b}=LEt(h,e);let w=b;const _={},{[d]:x,...T}=l;if(Object.entries(T||{}).forEach(([M,O])=>{const{vars:F,css:j,varsWithDefaults:W}=LEt(O,e);w=O_(w,W),_[M]={css:j,vars:F}}),x){const{css:M,vars:O,varsWithDefaults:F}=LEt(x,e);w=O_(w,F),_[d]={css:M,vars:O}}function I(M,O){let F=r;if(r==="class"&&(F=".%s"),r==="data"&&(F="[data-%s]"),r?.startsWith("data-")&&!r.includes("%s")&&(F=`[${r}="%s"]`),M){if(F==="media")return n.defaultColorScheme===M?":root":{[`@media (prefers-color-scheme: ${l[M]?.palette?.mode||M})`]:{":root":O}};if(F)return n.defaultColorScheme===M?`:root, ${F.replace("%s",String(M))}`:F.replace("%s",String(M))}return":root"}return{vars:w,generateThemeVars:()=>{let M={...p};return Object.entries(_).forEach(([,{vars:O}])=>{M=O_(M,O)}),M},generateStyleSheets:()=>{const M=[],O=n.defaultColorScheme||"light";function F(q,Z){Object.keys(Z).length&&M.push(typeof q=="string"?{[q]:{...Z}}:q)}F(t(void 0,{...m}),m);const{[O]:j,...W}=_;if(j){const{css:q}=j,Z=l[O]?.palette?.mode,ee=!i&&Z?{colorScheme:Z,...q}:{...q};F(t(O,{...ee}),ee)}return Object.entries(W).forEach(([q,{css:Z}])=>{const ee=l[q]?.palette?.mode,G=!i&&ee?{colorScheme:ee,...Z}:{...Z};F(t(q,{...G}),G)}),o&&M.push({":root":{"--__l-threshold":"0.7","--__l":"clamp(0, (l / var(--__l-threshold) - 1) * -infinity, 1)","--__a":"clamp(0.87, (l / var(--__l-threshold) - 1) * -infinity, 1)"}}),M}}}function jdr(n){return function(t){return n==="media"?`@media (prefers-color-scheme: ${t})`:n?n.startsWith("data-")&&!n.includes("%s")?`[${n}="${t}"] &`:n==="class"?`.${t} &`:n==="data"?`[data-${t}] &`:`${n.replace("%s",t)} &`:"&"}}function Fo(n,e,t=void 0){const i={};for(const r in n){const o=n[r];let l="",c=!0;for(let d=0;dn.filter(t=>e.includes(t)),q1e=(n,e,t)=>{const i=n.keys[0];Array.isArray(e)?e.forEach((r,o)=>{t((l,c)=>{o<=n.keys.length-1&&(o===0?Object.assign(l,c):l[n.up(n.keys[o])]=c)},r)}):e&&typeof e=="object"?(Object.keys(e).length>n.keys.length?n.keys:Hdr(n.keys,Object.keys(e))).forEach(o=>{if(n.keys.includes(o)){const l=e[o];l!==void 0&&t((c,d)=>{i===o?Object.assign(c,d):c[n.up(o)]=d},l)}}):(typeof e=="number"||typeof e=="string")&&t((r,o)=>{Object.assign(r,o)},e)};function Yqe(n){return`--Grid-${n}Spacing`}function Pet(n){return`--Grid-parent-${n}Spacing`}const WNn="--Grid-columns",Xpe="--Grid-parent-columns",Bdr=({theme:n,ownerState:e})=>{const t={};return q1e(n.breakpoints,e.size,(i,r)=>{let o={};r==="grow"&&(o={flexBasis:0,flexGrow:1,maxWidth:"100%"}),r==="auto"&&(o={flexBasis:"auto",flexGrow:0,flexShrink:0,maxWidth:"none",width:"auto"}),typeof r=="number"&&(o={flexGrow:0,flexBasis:"auto",width:`calc(100% * ${r} / var(${Xpe}) - (var(${Xpe}) - ${r}) * (var(${Pet("column")}) / var(${Xpe})))`}),i(t,o)}),t},Wdr=({theme:n,ownerState:e})=>{const t={};return q1e(n.breakpoints,e.offset,(i,r)=>{let o={};r==="auto"&&(o={marginLeft:"auto"}),typeof r=="number"&&(o={marginLeft:r===0?"0px":`calc(100% * ${r} / var(${Xpe}) + var(${Pet("column")}) * ${r} / var(${Xpe}))`}),i(t,o)}),t},Vdr=({theme:n,ownerState:e})=>{if(!e.container)return{};const t={[WNn]:12};return q1e(n.breakpoints,e.columns,(i,r)=>{const o=r??12;i(t,{[WNn]:o,"> *":{[Xpe]:o}})}),t},$dr=({theme:n,ownerState:e})=>{if(!e.container)return{};const t={};return q1e(n.breakpoints,e.rowSpacing,(i,r)=>{const o=typeof r=="string"?r:n.spacing?.(r);i(t,{[Yqe("row")]:o,"> *":{[Pet("row")]:o}})}),t},zdr=({theme:n,ownerState:e})=>{if(!e.container)return{};const t={};return q1e(n.breakpoints,e.columnSpacing,(i,r)=>{const o=typeof r=="string"?r:n.spacing?.(r);i(t,{[Yqe("column")]:o,"> *":{[Pet("column")]:o}})}),t},Udr=({theme:n,ownerState:e})=>{if(!e.container)return{};const t={};return q1e(n.breakpoints,e.direction,(i,r)=>{i(t,{flexDirection:r})}),t},qdr=({ownerState:n})=>({minWidth:0,boxSizing:"border-box",...n.container&&{display:"flex",flexWrap:"wrap",...n.wrap&&n.wrap!=="wrap"&&{flexWrap:n.wrap},gap:`var(${Yqe("row")}) var(${Yqe("column")})`}}),Gdr=n=>{const e=[];return Object.entries(n).forEach(([t,i])=>{i!==!1&&i!==void 0&&e.push(`grid-${t}-${String(i)}`)}),e},Kdr=(n,e="xs")=>{function t(i){return i===void 0?!1:typeof i=="string"&&!Number.isNaN(Number(i))||typeof i=="number"&&i>0}if(t(n))return[`spacing-${e}-${String(n)}`];if(typeof n=="object"&&!Array.isArray(n)){const i=[];return Object.entries(n).forEach(([r,o])=>{t(o)&&i.push(`spacing-${r}-${String(o)}`)}),i}return[]},Ydr=n=>n===void 0?[]:typeof n=="object"?Object.entries(n).map(([e,t])=>`direction-${e}-${t}`):[`direction-xs-${String(n)}`];function Zdr(n,e){n.item!==void 0&&delete n.item,n.zeroMinWidth!==void 0&&delete n.zeroMinWidth,e.keys.forEach(t=>{n[t]!==void 0&&delete n[t]})}const Xdr=h5e(),Qdr=loe("div",{name:"MuiGrid",slot:"Root"});function Jdr(n){return Azt({props:n,name:"MuiGrid",defaultTheme:Xdr})}function ehr(n={}){const{createStyledComponent:e=Qdr,useThemeProps:t=Jdr,useTheme:i=aoe,componentName:r="MuiGrid"}=n,o=(h,p)=>{const{container:m,direction:b,spacing:w,wrap:_,size:x}=h,T={root:["root",m&&"container",_!=="wrap"&&`wrap-xs-${String(_)}`,...Ydr(b),...Gdr(x),...m?Kdr(w,p.breakpoints.keys[0]):[]]};return Fo(T,I=>No(r,I),{})};function l(h,p,m=()=>!0){const b={};return h===null||(Array.isArray(h)?h.forEach((w,_)=>{w!==null&&m(w)&&p.keys[_]&&(b[p.keys[_]]=w)}):typeof h=="object"?Object.keys(h).forEach(w=>{const _=h[w];_!=null&&m(_)&&(b[w]=_)}):b[p.keys[0]]=h),b}const c=e(Vdr,zdr,$dr,Bdr,Udr,qdr,Wdr),d=D.forwardRef(function(p,m){const b=i(),w=t(p),_=Let(w);Zdr(_,b.breakpoints);const{className:x,children:T,columns:I=12,container:L=!1,component:A="div",direction:M="row",wrap:O="wrap",size:F={},offset:j={},spacing:W=0,rowSpacing:q=W,columnSpacing:Z=W,unstable_level:ee=0,...G}=_,te=l(F,b.breakpoints,ce=>ce!==!1),Q=l(j,b.breakpoints),ie=p.columns??(ee?void 0:I),se=p.spacing??(ee?void 0:W),de=p.rowSpacing??p.spacing??(ee?void 0:q),ne=p.columnSpacing??p.spacing??(ee?void 0:Z),we={..._,level:ee,columns:ie,container:L,direction:M,wrap:O,spacing:se,rowSpacing:de,columnSpacing:ne,size:te,offset:Q},ue=o(we,b);return k.jsx(c,{ref:m,as:A,ownerState:we,className:_i(ue.root,x),...G,children:D.Children.map(T,ce=>D.isValidElement(ce)&&n4e(ce,["Grid"])&&L&&ce.props.container?D.cloneElement(ce,{unstable_level:ce.props?.unstable_level??ee+1}):ce)})});return d.muiName="Grid",d}const thr=h5e(),nhr=loe("div",{name:"MuiStack",slot:"Root"});function ihr(n){return Azt({props:n,name:"MuiStack",defaultTheme:thr})}function rhr(n,e){const t=D.Children.toArray(n).filter(Boolean);return t.reduce((i,r,o)=>(i.push(r),o({row:"Left","row-reverse":"Right",column:"Top","column-reverse":"Bottom"})[n],ohr=({ownerState:n,theme:e})=>{let t={display:"flex",flexDirection:"column",...dM({theme:e},_Et({values:n.direction,breakpoints:e.breakpoints.values}),i=>({flexDirection:i}))};if(n.spacing){const i=_et(e),r=Object.keys(e.breakpoints.values).reduce((d,h)=>((typeof n.spacing=="object"&&n.spacing[h]!=null||typeof n.direction=="object"&&n.direction[h]!=null)&&(d[h]=!0),d),{}),o=_Et({values:n.direction,base:r}),l=_Et({values:n.spacing,base:r});typeof o=="object"&&Object.keys(o).forEach((d,h,p)=>{if(!o[d]){const b=h>0?o[p[h-1]]:"column";o[d]=b}}),t=O_(t,dM({theme:e},l,(d,h)=>n.useFlexGap?{gap:Qre(i,d)}:{"& > :not(style):not(style)":{margin:0},"& > :not(style) ~ :not(style)":{[`margin${shr(h?o[h]:n.direction)}`]:Qre(i,d)}}))}return t=rcr(e.breakpoints,t),t};function ahr(n={}){const{createStyledComponent:e=nhr,useThemeProps:t=ihr,componentName:i="MuiStack"}=n,r=()=>Fo({root:["root"]},d=>No(i,d),{}),o=e(ohr);return D.forwardRef(function(d,h){const p=t(d),m=Let(p),{component:b="div",direction:w="column",spacing:_=0,divider:x,children:T,className:I,useFlexGap:L=!1,...A}=m,M={direction:w,spacing:_,useFlexGap:L},O=r();return k.jsx(o,{as:b,ownerState:M,ref:h,className:_i(O.root,I),...A,children:x?rhr(T,x):T})})}const cLe={black:"#000",white:"#fff"},IOt={50:"#fafafa",100:"#f5f5f5",200:"#eeeeee",300:"#e0e0e0",400:"#bdbdbd",500:"#9e9e9e",600:"#757575",700:"#616161",800:"#424242",900:"#212121",A100:"#f5f5f5",A200:"#eeeeee",A400:"#bdbdbd",A700:"#616161"},the={50:"#f3e5f5",200:"#ce93d8",300:"#ba68c8",400:"#ab47bc",500:"#9c27b0",700:"#7b1fa2"},nhe={300:"#e57373",400:"#ef5350",500:"#f44336",700:"#d32f2f",800:"#c62828"},Nxe={300:"#ffb74d",400:"#ffa726",500:"#ff9800",700:"#f57c00",900:"#e65100"},qte={50:"#e3f2fd",200:"#90caf9",400:"#42a5f5",700:"#1976d2",800:"#1565c0"},ihe={300:"#4fc3f7",400:"#29b6f6",500:"#03a9f4",700:"#0288d1",900:"#01579b"},rhe={300:"#81c784",400:"#66bb6a",500:"#4caf50",700:"#388e3c",800:"#2e7d32",900:"#1b5e20"};function yci(){return{text:{primary:"rgba(0, 0, 0, 0.87)",secondary:"rgba(0, 0, 0, 0.6)",disabled:"rgba(0, 0, 0, 0.38)"},divider:"rgba(0, 0, 0, 0.12)",background:{paper:cLe.white,default:cLe.white},action:{active:"rgba(0, 0, 0, 0.54)",hover:"rgba(0, 0, 0, 0.04)",hoverOpacity:.04,selected:"rgba(0, 0, 0, 0.08)",selectedOpacity:.08,disabled:"rgba(0, 0, 0, 0.26)",disabledBackground:"rgba(0, 0, 0, 0.12)",disabledOpacity:.38,focus:"rgba(0, 0, 0, 0.12)",focusOpacity:.12,activatedOpacity:.12}}}const _ci=yci();function Cci(){return{text:{primary:cLe.white,secondary:"rgba(255, 255, 255, 0.7)",disabled:"rgba(255, 255, 255, 0.5)",icon:"rgba(255, 255, 255, 0.5)"},divider:"rgba(255, 255, 255, 0.12)",background:{paper:"#121212",default:"#121212"},action:{active:cLe.white,hover:"rgba(255, 255, 255, 0.08)",hoverOpacity:.08,selected:"rgba(255, 255, 255, 0.16)",selectedOpacity:.16,disabled:"rgba(255, 255, 255, 0.3)",disabledBackground:"rgba(255, 255, 255, 0.12)",disabledOpacity:.38,focus:"rgba(255, 255, 255, 0.12)",focusOpacity:.12,activatedOpacity:.24}}}const AOt=Cci();function VNn(n,e,t,i){const r=i.light||i,o=i.dark||i*1.5;n[e]||(n.hasOwnProperty(t)?n[e]=n[t]:e==="light"?n.light=Net(n.main,r):e==="dark"&&(n.dark=Oet(n.main,o)))}function $Nn(n,e,t,i,r){const o=r.light||r,l=r.dark||r*1.5;e[t]||(e.hasOwnProperty(i)?e[t]=e[i]:t==="light"?e.light=`color-mix(in ${n}, ${e.main}, #fff ${(o*100).toFixed(0)}%)`:t==="dark"&&(e.dark=`color-mix(in ${n}, ${e.main}, #000 ${(l*100).toFixed(0)}%)`))}function lhr(n="light"){return n==="dark"?{main:qte[200],light:qte[50],dark:qte[400]}:{main:qte[700],light:qte[400],dark:qte[800]}}function chr(n="light"){return n==="dark"?{main:the[200],light:the[50],dark:the[400]}:{main:the[500],light:the[300],dark:the[700]}}function uhr(n="light"){return n==="dark"?{main:nhe[500],light:nhe[300],dark:nhe[700]}:{main:nhe[700],light:nhe[400],dark:nhe[800]}}function dhr(n="light"){return n==="dark"?{main:ihe[400],light:ihe[300],dark:ihe[700]}:{main:ihe[700],light:ihe[500],dark:ihe[900]}}function hhr(n="light"){return n==="dark"?{main:rhe[400],light:rhe[300],dark:rhe[700]}:{main:rhe[800],light:rhe[500],dark:rhe[900]}}function fhr(n="light"){return n==="dark"?{main:Nxe[400],light:Nxe[300],dark:Nxe[700]}:{main:"#ed6c02",light:Nxe[500],dark:Nxe[900]}}function phr(n){return`oklch(from ${n} var(--__l) 0 h / var(--__a))`}function Pzt(n){const{mode:e="light",contrastThreshold:t=3,tonalOffset:i=.2,colorSpace:r,...o}=n,l=n.primary||lhr(e),c=n.secondary||chr(e),d=n.error||uhr(e),h=n.info||dhr(e),p=n.success||hhr(e),m=n.warning||fhr(e);function b(T){return r?phr(T):fdr(T,AOt.text.primary)>=t?AOt.text.primary:_ci.text.primary}const w=({color:T,name:I,mainShade:L=500,lightShade:A=300,darkShade:M=700})=>{if(T={...T},!T.main&&T[L]&&(T.main=T[L]),!T.hasOwnProperty("main"))throw new Error(bW(11,I?` (${I})`:"",L));if(typeof T.main!="string")throw new Error(bW(12,I?` (${I})`:"",JSON.stringify(T.main)));return r?($Nn(r,T,"light",A,i),$Nn(r,T,"dark",M,i)):(VNn(T,"light",A,i),VNn(T,"dark",M,i)),T.contrastText||(T.contrastText=b(T.main)),T};let _;return e==="light"?_=yci():e==="dark"&&(_=Cci()),O_({common:{...cLe},mode:e,primary:w({color:l,name:"primary"}),secondary:w({color:c,name:"secondary",mainShade:"A400",lightShade:"A200",darkShade:"A700"}),error:w({color:d,name:"error"}),warning:w({color:m,name:"warning"}),info:w({color:h,name:"info"}),success:w({color:p,name:"success"}),grey:IOt,contrastThreshold:t,getContrastText:b,augmentColor:w,tonalOffset:i,..._},o)}function ghr(n){const e={};return Object.entries(n).forEach(i=>{const[r,o]=i;typeof o=="object"&&(e[r]=`${o.fontStyle?`${o.fontStyle} `:""}${o.fontVariant?`${o.fontVariant} `:""}${o.fontWeight?`${o.fontWeight} `:""}${o.fontStretch?`${o.fontStretch} `:""}${o.fontSize||""}${o.lineHeight?`/${o.lineHeight} `:""}${o.fontFamily||""}`)}),e}function mhr(n,e){return{toolbar:{minHeight:56,[n.up("xs")]:{"@media (orientation: landscape)":{minHeight:48}},[n.up("sm")]:{minHeight:64}},...e}}function bhr(n){return Math.round(n*1e5)/1e5}const zNn={textTransform:"uppercase"},UNn='"Roboto", "Helvetica", "Arial", sans-serif';function Sci(n,e){const{fontFamily:t=UNn,fontSize:i=14,fontWeightLight:r=300,fontWeightRegular:o=400,fontWeightMedium:l=500,fontWeightBold:c=700,htmlFontSize:d=16,allVariants:h,pxToRem:p,...m}=typeof e=="function"?e(n):e,b=i/14,w=p||(T=>`${T/d*b}rem`),_=(T,I,L,A,M)=>({fontFamily:t,fontWeight:T,fontSize:w(I),lineHeight:L,...t===UNn?{letterSpacing:`${bhr(A/I)}em`}:{},...M,...h}),x={h1:_(r,96,1.167,-1.5),h2:_(r,60,1.2,-.5),h3:_(o,48,1.167,0),h4:_(o,34,1.235,.25),h5:_(o,24,1.334,0),h6:_(l,20,1.6,.15),subtitle1:_(o,16,1.75,.15),subtitle2:_(l,14,1.57,.1),body1:_(o,16,1.5,.15),body2:_(o,14,1.43,.15),button:_(l,14,1.75,.4,zNn),caption:_(o,12,1.66,.4),overline:_(o,12,2.66,1,zNn),inherit:{fontFamily:"inherit",fontWeight:"inherit",fontSize:"inherit",lineHeight:"inherit",letterSpacing:"inherit"}};return O_({htmlFontSize:d,pxToRem:w,fontFamily:t,fontSize:i,fontWeightLight:r,fontWeightRegular:o,fontWeightMedium:l,fontWeightBold:c,...x},m,{clone:!1})}const vhr=.2,whr=.14,yhr=.12;function C1(...n){return[`${n[0]}px ${n[1]}px ${n[2]}px ${n[3]}px rgba(0,0,0,${vhr})`,`${n[4]}px ${n[5]}px ${n[6]}px ${n[7]}px rgba(0,0,0,${whr})`,`${n[8]}px ${n[9]}px ${n[10]}px ${n[11]}px rgba(0,0,0,${yhr})`].join(",")}const _hr=["none",C1(0,2,1,-1,0,1,1,0,0,1,3,0),C1(0,3,1,-2,0,2,2,0,0,1,5,0),C1(0,3,3,-2,0,3,4,0,0,1,8,0),C1(0,2,4,-1,0,4,5,0,0,1,10,0),C1(0,3,5,-1,0,5,8,0,0,1,14,0),C1(0,3,5,-1,0,6,10,0,0,1,18,0),C1(0,4,5,-2,0,7,10,1,0,2,16,1),C1(0,5,5,-3,0,8,10,1,0,3,14,2),C1(0,5,6,-3,0,9,12,1,0,3,16,2),C1(0,6,6,-3,0,10,14,1,0,4,18,3),C1(0,6,7,-4,0,11,15,1,0,4,20,3),C1(0,7,8,-4,0,12,17,2,0,5,22,4),C1(0,7,8,-4,0,13,19,2,0,5,24,4),C1(0,7,9,-4,0,14,21,2,0,5,26,4),C1(0,8,9,-5,0,15,22,2,0,6,28,5),C1(0,8,10,-5,0,16,24,2,0,6,30,5),C1(0,8,11,-5,0,17,26,2,0,6,32,5),C1(0,9,11,-5,0,18,28,2,0,7,34,6),C1(0,9,12,-6,0,19,29,2,0,7,36,6),C1(0,10,13,-6,0,20,31,3,0,8,38,7),C1(0,10,13,-6,0,21,33,3,0,8,40,7),C1(0,10,14,-6,0,22,35,3,0,8,42,7),C1(0,11,14,-7,0,23,36,3,0,9,44,8),C1(0,11,15,-7,0,24,38,3,0,9,46,8)],Chr={easeInOut:"cubic-bezier(0.4, 0, 0.2, 1)",easeOut:"cubic-bezier(0.0, 0, 0.2, 1)",easeIn:"cubic-bezier(0.4, 0, 1, 1)",sharp:"cubic-bezier(0.4, 0, 0.6, 1)"},xci={shortest:150,shorter:200,short:250,standard:300,complex:375,enteringScreen:225,leavingScreen:195};function qNn(n){return`${Math.round(n)}ms`}function Shr(n){if(!n)return 0;const e=n/36;return Math.min(Math.round((4+15*e**.25+e/5)*10),3e3)}function xhr(n){const e={...Chr,...n.easing},t={...xci,...n.duration};return{getAutoHeightDuration:Shr,create:(r=["all"],o={})=>{const{duration:l=t.standard,easing:c=e.easeInOut,delay:d=0,...h}=o;return(Array.isArray(r)?r:[r]).map(p=>`${p} ${typeof l=="string"?l:qNn(l)} ${c} ${typeof d=="string"?d:qNn(d)}`).join(",")},...n,easing:e,duration:t}}const Ehr={mobileStepper:1e3,fab:1050,speedDial:1050,appBar:1100,drawer:1200,modal:1300,snackbar:1400,tooltip:1500};function khr(n){return KP(n)||typeof n>"u"||typeof n=="string"||typeof n=="boolean"||typeof n=="number"||Array.isArray(n)}function Eci(n={}){const e={...n};function t(i){const r=Object.entries(i);for(let o=0;o(!e&&typeof window<"u"&&(e=window),{get(t){if(typeof window>"u")return;if(!e)return t;let i;try{i=e.localStorage.getItem(n)}catch{}return i||t},set:t=>{if(e)try{e.localStorage.setItem(n,t)}catch{}},subscribe:t=>{if(!e)return Odr;const i=r=>{const o=r.newValue;r.key===n&&t(o)};return e.addEventListener("storage",i),()=>{e.removeEventListener("storage",i)}}});function TEt(){}function BNn(n){if(typeof window<"u"&&typeof window.matchMedia=="function"&&n==="system")return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function Cci(n,e){if(n.mode==="light"||n.mode==="system"&&n.systemMode==="light")return e("light");if(n.mode==="dark"||n.mode==="system"&&n.systemMode==="dark")return e("dark")}function Pdr(n){return Cci(n,e=>{if(e==="light")return n.lightColorScheme;if(e==="dark")return n.darkColorScheme})}function Fdr(n){const{defaultMode:e="light",defaultLightColorScheme:t,defaultDarkColorScheme:i,supportedColorSchemes:r=[],modeStorageKey:o=Rzt,colorSchemeStorageKey:l=Mzt,storageWindow:c=typeof window>"u"?void 0:window,storageManager:d=Ndr,noSsr:h=!1}=n,p=r.join(","),m=r.length>1,b=L.useMemo(()=>d?.({key:o,storageWindow:c}),[d,o,c]),w=L.useMemo(()=>d?.({key:`${l}-light`,storageWindow:c}),[d,l,c]),_=L.useMemo(()=>d?.({key:`${l}-dark`,storageWindow:c}),[d,l,c]),[x,T]=L.useState(()=>{const W=b?.get(e)||e,U=w?.get(t)||t,Z=_?.get(i)||i;return{mode:W,systemMode:BNn(W),lightColorScheme:U,darkColorScheme:Z}}),[I,D]=L.useState(h||!m);L.useEffect(()=>{D(!0)},[]);const A=Pdr(x),M=L.useCallback(W=>{T(U=>{if(W===U.mode)return U;const Z=W??e;return b?.set(Z),{...U,mode:Z,systemMode:BNn(Z)}})},[b,e]),O=L.useCallback(W=>{W?typeof W=="string"?W&&!p.includes(W)?console.error(`\`${W}\` does not exist in \`theme.colorSchemes\`.`):T(U=>{const Z={...U};return Cci(U,te=>{te==="light"&&(w?.set(W),Z.lightColorScheme=W),te==="dark"&&(_?.set(W),Z.darkColorScheme=W)}),Z}):T(U=>{const Z={...U},te=W.light===null?t:W.light,G=W.dark===null?i:W.dark;return te&&(p.includes(te)?(Z.lightColorScheme=te,w?.set(te)):console.error(`\`${te}\` does not exist in \`theme.colorSchemes\`.`)),G&&(p.includes(G)?(Z.darkColorScheme=G,_?.set(G)):console.error(`\`${G}\` does not exist in \`theme.colorSchemes\`.`)),Z}):T(U=>(w?.set(t),_?.set(i),{...U,lightColorScheme:t,darkColorScheme:i}))},[p,w,_,t,i]),F=L.useCallback(W=>{x.mode==="system"&&T(U=>{const Z=W?.matches?"dark":"light";return U.systemMode===Z?U:{...U,systemMode:Z}})},[x.mode]),j=L.useRef(F);return j.current=F,L.useEffect(()=>{if(typeof window.matchMedia!="function"||!m)return;const W=(...Z)=>j.current(...Z),U=window.matchMedia("(prefers-color-scheme: dark)");return U.addListener(W),W(U),()=>{U.removeListener(W)}},[m]),L.useEffect(()=>{if(m){const W=b?.subscribe(te=>{(!te||["light","dark","system"].includes(te))&&M(te||e)})||TEt,U=w?.subscribe(te=>{(!te||p.match(te))&&O({light:te})})||TEt,Z=_?.subscribe(te=>{(!te||p.match(te))&&O({dark:te})})||TEt;return()=>{W(),U(),Z()}}},[O,M,p,e,c,m,b,w,_]),{...x,mode:I?x.mode:void 0,systemMode:I?x.systemMode:void 0,colorScheme:I?A:void 0,setMode:M,setColorScheme:O}}const jdr="*{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}";function Hdr(n){const{themeId:e,theme:t={},modeStorageKey:i=Rzt,colorSchemeStorageKey:r=Mzt,disableTransitionOnChange:o=!1,defaultColorScheme:l,resolveTheme:c}=n,d={allColorSchemes:[],colorScheme:void 0,darkColorScheme:void 0,lightColorScheme:void 0,mode:void 0,setColorScheme:()=>{},setMode:()=>{},systemMode:void 0},h=L.createContext(void 0),p=()=>L.useContext(h)||d,m={},b={};function w(I){const{children:D,theme:A,modeStorageKey:M=i,colorSchemeStorageKey:O=r,disableTransitionOnChange:F=o,storageManager:j,storageWindow:W=typeof window>"u"?void 0:window,documentNode:U=typeof document>"u"?void 0:document,colorSchemeNode:Z=typeof document>"u"?void 0:document.documentElement,disableNestedContext:te=!1,disableStyleSheetGeneration:G=!1,defaultMode:ee="system",forceThemeRerender:Q=!1,noSsr:ie}=I,se=L.useRef(!1),ue=Azt(),ne=L.useContext(h),we=!!ne&&!te,de=L.useMemo(()=>A||(typeof t=="function"?t():t),[A]),ce=de[e],ye=ce||de,{colorSchemes:he=m,components:pe=b,cssVarPrefix:me}=ye,be=Object.keys(he).filter(it=>!!he[it]).join(","),xe=L.useMemo(()=>be.split(","),[be]),Te=typeof l=="string"?l:l.light,qe=typeof l=="string"?l:l.dark,et=he[Te]&&he[qe]?ee:he[ye.defaultColorScheme]?.palette?.mode||ye.palette?.mode,{mode:Ge,setMode:Me,systemMode:He,lightColorScheme:lt,darkColorScheme:st,colorScheme:Be,setColorScheme:ot}=Fdr({supportedColorSchemes:xe,defaultLightColorScheme:Te,defaultDarkColorScheme:qe,modeStorageKey:M,colorSchemeStorageKey:O,defaultMode:et,storageManager:j,storageWindow:W,noSsr:ie});let ct=Ge,ze=Be;we&&(ct=ne.mode,ze=ne.colorScheme);let Ke=ze||ye.defaultColorScheme;ye.vars&&!Q&&(Ke=ye.defaultColorScheme);const $e=L.useMemo(()=>{const it=ye.generateThemeVars?.()||ye.vars,St={...ye,components:pe,colorSchemes:he,cssVarPrefix:me,vars:it};if(typeof St.generateSpacing=="function"&&(St.spacing=St.generateSpacing()),Ke){const Ot=he[Ke];Ot&&typeof Ot=="object"&&Object.keys(Ot).forEach(Jt=>{Ot[Jt]&&typeof Ot[Jt]=="object"?St[Jt]={...St[Jt],...Ot[Jt]}:St[Jt]=Ot[Jt]})}return c?c(St):St},[ye,Ke,pe,he,me]),tt=ye.colorSchemeSelector;IS(()=>{if(ze&&Z&&tt&&tt!=="media"){const it=tt;let St=tt;if(it==="class"&&(St=".%s"),it==="data"&&(St="[data-%s]"),it?.startsWith("data-")&&!it.includes("%s")&&(St=`[${it}="%s"]`),St.startsWith("."))Z.classList.remove(...xe.map(Ot=>St.substring(1).replace("%s",Ot))),Z.classList.add(St.substring(1).replace("%s",ze));else{const Ot=St.replace("%s",ze).match(/\[([^\]]+)\]/);if(Ot){const[Jt,fn]=Ot[1].split("=");fn||xe.forEach(dn=>{Z.removeAttribute(Jt.replace(ze,dn))}),Z.setAttribute(Jt,fn?fn.replace(/"|'/g,""):"")}else Z.setAttribute(St,ze)}}},[ze,tt,Z,xe]),L.useEffect(()=>{let it;if(F&&se.current&&U){const St=U.createElement("style");St.appendChild(U.createTextNode(jdr)),U.head.appendChild(St),window.getComputedStyle(U.body),it=setTimeout(()=>{U.head.removeChild(St)},1)}return()=>{clearTimeout(it)}},[ze,F,U]),L.useEffect(()=>(se.current=!0,()=>{se.current=!1}),[]);const vt=L.useMemo(()=>({allColorSchemes:xe,colorScheme:ze,darkColorScheme:st,lightColorScheme:lt,mode:ct,setColorScheme:ot,setMode:Me,systemMode:He}),[xe,ze,st,lt,ct,ot,Me,He,$e.colorSchemeSelector]);let Ft=!0;(G||ye.cssVariables===!1||we&&ue?.cssVarPrefix===me)&&(Ft=!1);const _t=k.jsxs(L.Fragment,{children:[k.jsx(_ci,{themeId:ce?e:void 0,theme:$e,children:D}),Ft&&k.jsx(sci,{styles:$e.generateStyleSheets?.()||[]})]});return we?_t:k.jsx(h.Provider,{value:vt,children:_t})}const _=typeof l=="string"?l:l.light,x=typeof l=="string"?l:l.dark;return{CssVarsProvider:w,useColorScheme:p,getInitColorSchemeScript:I=>Mdr({colorSchemeStorageKey:r,defaultLightColorScheme:_,defaultDarkColorScheme:x,modeStorageKey:i,...I})}}function Bdr(n=""){function e(...i){if(!i.length)return"";const r=i[0];return typeof r=="string"&&!r.match(/(#|\(|\)|(-?(\d*\.)?\d+)(px|em|%|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc))|^(-?(\d*\.)?\d+)$|(\d+ \d+ \d+)/)?`, var(--${n?`${n}-`:""}${r}${e(...i.slice(1))})`:`, ${r}`}return(i,...r)=>`var(--${n?`${n}-`:""}${i}${e(...r)})`}const WNn=(n,e,t,i=[])=>{let r=n;e.forEach((o,l)=>{l===e.length-1?Array.isArray(r)?r[Number(o)]=t:r&&typeof r=="object"&&(r[o]=t):r&&typeof r=="object"&&(r[o]||(r[o]=i.includes(o)?[]:{}),r=r[o])})},Wdr=(n,e,t)=>{function i(r,o=[],l=[]){Object.entries(r).forEach(([c,d])=>{(!t||t&&!t([...o,c]))&&d!=null&&(typeof d=="object"&&Object.keys(d).length>0?i(d,[...o,c],Array.isArray(d)?[...l,c]:l):e([...o,c],d,l))})}i(n)},Vdr=(n,e)=>typeof e=="number"?["lineHeight","fontWeight","opacity","zIndex"].some(i=>n.includes(i))||n[n.length-1].toLowerCase().includes("opacity")?e:`${e}px`:e;function LEt(n,e){const{prefix:t,shouldSkipGeneratingVar:i}=e||{},r={},o={},l={};return Wdr(n,(c,d,h)=>{if((typeof d=="string"||typeof d=="number")&&(!i||!i(c,d))){const p=`--${t?`${t}-`:""}${c.join("-")}`,m=Vdr(c,d);Object.assign(r,{[p]:m}),WNn(o,c,`var(${p})`,h),WNn(l,c,`var(${p}, ${m})`,h)}},c=>c[0]==="vars"),{css:r,vars:o,varsWithDefaults:l}}function $dr(n,e={}){const{getSelector:t=I,disableCssColorScheme:i,colorSchemeSelector:r,enableContrastVars:o}=e,{colorSchemes:l={},components:c,defaultColorScheme:d="light",...h}=n,{vars:p,css:m,varsWithDefaults:b}=LEt(h,e);let w=b;const _={},{[d]:x,...T}=l;if(Object.entries(T||{}).forEach(([M,O])=>{const{vars:F,css:j,varsWithDefaults:W}=LEt(O,e);w=F_(w,W),_[M]={css:j,vars:F}}),x){const{css:M,vars:O,varsWithDefaults:F}=LEt(x,e);w=F_(w,F),_[d]={css:M,vars:O}}function I(M,O){let F=r;if(r==="class"&&(F=".%s"),r==="data"&&(F="[data-%s]"),r?.startsWith("data-")&&!r.includes("%s")&&(F=`[${r}="%s"]`),M){if(F==="media")return n.defaultColorScheme===M?":root":{[`@media (prefers-color-scheme: ${l[M]?.palette?.mode||M})`]:{":root":O}};if(F)return n.defaultColorScheme===M?`:root, ${F.replace("%s",String(M))}`:F.replace("%s",String(M))}return":root"}return{vars:w,generateThemeVars:()=>{let M={...p};return Object.entries(_).forEach(([,{vars:O}])=>{M=F_(M,O)}),M},generateStyleSheets:()=>{const M=[],O=n.defaultColorScheme||"light";function F(U,Z){Object.keys(Z).length&&M.push(typeof U=="string"?{[U]:{...Z}}:U)}F(t(void 0,{...m}),m);const{[O]:j,...W}=_;if(j){const{css:U}=j,Z=l[O]?.palette?.mode,te=!i&&Z?{colorScheme:Z,...U}:{...U};F(t(O,{...te}),te)}return Object.entries(W).forEach(([U,{css:Z}])=>{const te=l[U]?.palette?.mode,G=!i&&te?{colorScheme:te,...Z}:{...Z};F(t(U,{...G}),G)}),o&&M.push({":root":{"--__l-threshold":"0.7","--__l":"clamp(0, (l / var(--__l-threshold) - 1) * -infinity, 1)","--__a":"clamp(0.87, (l / var(--__l-threshold) - 1) * -infinity, 1)"}}),M}}}function zdr(n){return function(t){return n==="media"?`@media (prefers-color-scheme: ${t})`:n?n.startsWith("data-")&&!n.includes("%s")?`[${n}="${t}"] &`:n==="class"?`.${t} &`:n==="data"?`[data-${t}] &`:`${n.replace("%s",t)} &`:"&"}}function jo(n,e,t=void 0){const i={};for(const r in n){const o=n[r];let l="",c=!0;for(let d=0;dn.filter(t=>e.includes(t)),V1e=(n,e,t)=>{const i=n.keys[0];Array.isArray(e)?e.forEach((r,o)=>{t((l,c)=>{o<=n.keys.length-1&&(o===0?Object.assign(l,c):l[n.up(n.keys[o])]=c)},r)}):e&&typeof e=="object"?(Object.keys(e).length>n.keys.length?n.keys:Udr(n.keys,Object.keys(e))).forEach(o=>{if(n.keys.includes(o)){const l=e[o];l!==void 0&&t((c,d)=>{i===o?Object.assign(c,d):c[n.up(o)]=d},l)}}):(typeof e=="number"||typeof e=="string")&&t((r,o)=>{Object.assign(r,o)},e)};function Yqe(n){return`--Grid-${n}Spacing`}function Pet(n){return`--Grid-parent-${n}Spacing`}const VNn="--Grid-columns",Kpe="--Grid-parent-columns",qdr=({theme:n,ownerState:e})=>{const t={};return V1e(n.breakpoints,e.size,(i,r)=>{let o={};r==="grow"&&(o={flexBasis:0,flexGrow:1,maxWidth:"100%"}),r==="auto"&&(o={flexBasis:"auto",flexGrow:0,flexShrink:0,maxWidth:"none",width:"auto"}),typeof r=="number"&&(o={flexGrow:0,flexBasis:"auto",width:`calc(100% * ${r} / var(${Kpe}) - (var(${Kpe}) - ${r}) * (var(${Pet("column")}) / var(${Kpe})))`}),i(t,o)}),t},Gdr=({theme:n,ownerState:e})=>{const t={};return V1e(n.breakpoints,e.offset,(i,r)=>{let o={};r==="auto"&&(o={marginLeft:"auto"}),typeof r=="number"&&(o={marginLeft:r===0?"0px":`calc(100% * ${r} / var(${Kpe}) + var(${Pet("column")}) * ${r} / var(${Kpe}))`}),i(t,o)}),t},Kdr=({theme:n,ownerState:e})=>{if(!e.container)return{};const t={[VNn]:12};return V1e(n.breakpoints,e.columns,(i,r)=>{const o=r??12;i(t,{[VNn]:o,"> *":{[Kpe]:o}})}),t},Ydr=({theme:n,ownerState:e})=>{if(!e.container)return{};const t={};return V1e(n.breakpoints,e.rowSpacing,(i,r)=>{const o=typeof r=="string"?r:n.spacing?.(r);i(t,{[Yqe("row")]:o,"> *":{[Pet("row")]:o}})}),t},Zdr=({theme:n,ownerState:e})=>{if(!e.container)return{};const t={};return V1e(n.breakpoints,e.columnSpacing,(i,r)=>{const o=typeof r=="string"?r:n.spacing?.(r);i(t,{[Yqe("column")]:o,"> *":{[Pet("column")]:o}})}),t},Xdr=({theme:n,ownerState:e})=>{if(!e.container)return{};const t={};return V1e(n.breakpoints,e.direction,(i,r)=>{i(t,{flexDirection:r})}),t},Qdr=({ownerState:n})=>({minWidth:0,boxSizing:"border-box",...n.container&&{display:"flex",flexWrap:"wrap",...n.wrap&&n.wrap!=="wrap"&&{flexWrap:n.wrap},gap:`var(${Yqe("row")}) var(${Yqe("column")})`}}),Jdr=n=>{const e=[];return Object.entries(n).forEach(([t,i])=>{i!==!1&&i!==void 0&&e.push(`grid-${t}-${String(i)}`)}),e},ehr=(n,e="xs")=>{function t(i){return i===void 0?!1:typeof i=="string"&&!Number.isNaN(Number(i))||typeof i=="number"&&i>0}if(t(n))return[`spacing-${e}-${String(n)}`];if(typeof n=="object"&&!Array.isArray(n)){const i=[];return Object.entries(n).forEach(([r,o])=>{t(o)&&i.push(`spacing-${r}-${String(o)}`)}),i}return[]},thr=n=>n===void 0?[]:typeof n=="object"?Object.entries(n).map(([e,t])=>`direction-${e}-${t}`):[`direction-xs-${String(n)}`];function nhr(n,e){n.item!==void 0&&delete n.item,n.zeroMinWidth!==void 0&&delete n.zeroMinWidth,e.keys.forEach(t=>{n[t]!==void 0&&delete n[t]})}const ihr=d5e(),rhr=aoe("div",{name:"MuiGrid",slot:"Root"});function shr(n){return Dzt({props:n,name:"MuiGrid",defaultTheme:ihr})}function ohr(n={}){const{createStyledComponent:e=rhr,useThemeProps:t=shr,useTheme:i=ooe,componentName:r="MuiGrid"}=n,o=(h,p)=>{const{container:m,direction:b,spacing:w,wrap:_,size:x}=h,T={root:["root",m&&"container",_!=="wrap"&&`wrap-xs-${String(_)}`,...thr(b),...Jdr(x),...m?ehr(w,p.breakpoints.keys[0]):[]]};return jo(T,I=>Po(r,I),{})};function l(h,p,m=()=>!0){const b={};return h===null||(Array.isArray(h)?h.forEach((w,_)=>{w!==null&&m(w)&&p.keys[_]&&(b[p.keys[_]]=w)}):typeof h=="object"?Object.keys(h).forEach(w=>{const _=h[w];_!=null&&m(_)&&(b[w]=_)}):b[p.keys[0]]=h),b}const c=e(Kdr,Zdr,Ydr,qdr,Xdr,Qdr,Gdr),d=L.forwardRef(function(p,m){const b=i(),w=t(p),_=Let(w);nhr(_,b.breakpoints);const{className:x,children:T,columns:I=12,container:D=!1,component:A="div",direction:M="row",wrap:O="wrap",size:F={},offset:j={},spacing:W=0,rowSpacing:U=W,columnSpacing:Z=W,unstable_level:te=0,...G}=_,ee=l(F,b.breakpoints,ce=>ce!==!1),Q=l(j,b.breakpoints),ie=p.columns??(te?void 0:I),se=p.spacing??(te?void 0:W),ue=p.rowSpacing??p.spacing??(te?void 0:U),ne=p.columnSpacing??p.spacing??(te?void 0:Z),we={..._,level:te,columns:ie,container:D,direction:M,wrap:O,spacing:se,rowSpacing:ue,columnSpacing:ne,size:ee,offset:Q},de=o(we,b);return k.jsx(c,{ref:m,as:A,ownerState:we,className:_i(de.root,x),...G,children:L.Children.map(T,ce=>L.isValidElement(ce)&&e4e(ce,["Grid"])&&D&&ce.props.container?L.cloneElement(ce,{unstable_level:ce.props?.unstable_level??te+1}):ce)})});return d.muiName="Grid",d}const ahr=d5e(),lhr=aoe("div",{name:"MuiStack",slot:"Root"});function chr(n){return Dzt({props:n,name:"MuiStack",defaultTheme:ahr})}function uhr(n,e){const t=L.Children.toArray(n).filter(Boolean);return t.reduce((i,r,o)=>(i.push(r),o({row:"Left","row-reverse":"Right",column:"Top","column-reverse":"Bottom"})[n],hhr=({ownerState:n,theme:e})=>{let t={display:"flex",flexDirection:"column",...dM({theme:e},_Et({values:n.direction,breakpoints:e.breakpoints.values}),i=>({flexDirection:i}))};if(n.spacing){const i=_et(e),r=Object.keys(e.breakpoints.values).reduce((d,h)=>((typeof n.spacing=="object"&&n.spacing[h]!=null||typeof n.direction=="object"&&n.direction[h]!=null)&&(d[h]=!0),d),{}),o=_Et({values:n.direction,base:r}),l=_Et({values:n.spacing,base:r});typeof o=="object"&&Object.keys(o).forEach((d,h,p)=>{if(!o[d]){const b=h>0?o[p[h-1]]:"column";o[d]=b}}),t=F_(t,dM({theme:e},l,(d,h)=>n.useFlexGap?{gap:Xre(i,d)}:{"& > :not(style):not(style)":{margin:0},"& > :not(style) ~ :not(style)":{[`margin${dhr(h?o[h]:n.direction)}`]:Xre(i,d)}}))}return t=ucr(e.breakpoints,t),t};function fhr(n={}){const{createStyledComponent:e=lhr,useThemeProps:t=chr,componentName:i="MuiStack"}=n,r=()=>jo({root:["root"]},d=>Po(i,d),{}),o=e(hhr);return L.forwardRef(function(d,h){const p=t(d),m=Let(p),{component:b="div",direction:w="column",spacing:_=0,divider:x,children:T,className:I,useFlexGap:D=!1,...A}=m,M={direction:w,spacing:_,useFlexGap:D},O=r();return k.jsx(o,{as:b,ownerState:M,ref:h,className:_i(O.root,I),...A,children:x?uhr(T,x):T})})}const a6e={black:"#000",white:"#fff"},DOt={50:"#fafafa",100:"#f5f5f5",200:"#eeeeee",300:"#e0e0e0",400:"#bdbdbd",500:"#9e9e9e",600:"#757575",700:"#616161",800:"#424242",900:"#212121",A100:"#f5f5f5",A200:"#eeeeee",A400:"#bdbdbd",A700:"#616161"},Qde={50:"#f3e5f5",200:"#ce93d8",300:"#ba68c8",400:"#ab47bc",500:"#9c27b0",700:"#7b1fa2"},Jde={300:"#e57373",400:"#ef5350",500:"#f44336",700:"#d32f2f",800:"#c62828"},Mxe={300:"#ffb74d",400:"#ffa726",500:"#ff9800",700:"#f57c00",900:"#e65100"},Ute={50:"#e3f2fd",200:"#90caf9",400:"#42a5f5",700:"#1976d2",800:"#1565c0"},ehe={300:"#4fc3f7",400:"#29b6f6",500:"#03a9f4",700:"#0288d1",900:"#01579b"},the={300:"#81c784",400:"#66bb6a",500:"#4caf50",700:"#388e3c",800:"#2e7d32",900:"#1b5e20"};function Sci(){return{text:{primary:"rgba(0, 0, 0, 0.87)",secondary:"rgba(0, 0, 0, 0.6)",disabled:"rgba(0, 0, 0, 0.38)"},divider:"rgba(0, 0, 0, 0.12)",background:{paper:a6e.white,default:a6e.white},action:{active:"rgba(0, 0, 0, 0.54)",hover:"rgba(0, 0, 0, 0.04)",hoverOpacity:.04,selected:"rgba(0, 0, 0, 0.08)",selectedOpacity:.08,disabled:"rgba(0, 0, 0, 0.26)",disabledBackground:"rgba(0, 0, 0, 0.12)",disabledOpacity:.38,focus:"rgba(0, 0, 0, 0.12)",focusOpacity:.12,activatedOpacity:.12}}}const xci=Sci();function Eci(){return{text:{primary:a6e.white,secondary:"rgba(255, 255, 255, 0.7)",disabled:"rgba(255, 255, 255, 0.5)",icon:"rgba(255, 255, 255, 0.5)"},divider:"rgba(255, 255, 255, 0.12)",background:{paper:"#121212",default:"#121212"},action:{active:a6e.white,hover:"rgba(255, 255, 255, 0.08)",hoverOpacity:.08,selected:"rgba(255, 255, 255, 0.16)",selectedOpacity:.16,disabled:"rgba(255, 255, 255, 0.3)",disabledBackground:"rgba(255, 255, 255, 0.12)",disabledOpacity:.38,focus:"rgba(255, 255, 255, 0.12)",focusOpacity:.12,activatedOpacity:.24}}}const IOt=Eci();function $Nn(n,e,t,i){const r=i.light||i,o=i.dark||i*1.5;n[e]||(n.hasOwnProperty(t)?n[e]=n[t]:e==="light"?n.light=Net(n.main,r):e==="dark"&&(n.dark=Oet(n.main,o)))}function zNn(n,e,t,i,r){const o=r.light||r,l=r.dark||r*1.5;e[t]||(e.hasOwnProperty(i)?e[t]=e[i]:t==="light"?e.light=`color-mix(in ${n}, ${e.main}, #fff ${(o*100).toFixed(0)}%)`:t==="dark"&&(e.dark=`color-mix(in ${n}, ${e.main}, #000 ${(l*100).toFixed(0)}%)`))}function phr(n="light"){return n==="dark"?{main:Ute[200],light:Ute[50],dark:Ute[400]}:{main:Ute[700],light:Ute[400],dark:Ute[800]}}function ghr(n="light"){return n==="dark"?{main:Qde[200],light:Qde[50],dark:Qde[400]}:{main:Qde[500],light:Qde[300],dark:Qde[700]}}function mhr(n="light"){return n==="dark"?{main:Jde[500],light:Jde[300],dark:Jde[700]}:{main:Jde[700],light:Jde[400],dark:Jde[800]}}function bhr(n="light"){return n==="dark"?{main:ehe[400],light:ehe[300],dark:ehe[700]}:{main:ehe[700],light:ehe[500],dark:ehe[900]}}function vhr(n="light"){return n==="dark"?{main:the[400],light:the[300],dark:the[700]}:{main:the[800],light:the[500],dark:the[900]}}function whr(n="light"){return n==="dark"?{main:Mxe[400],light:Mxe[300],dark:Mxe[700]}:{main:"#ed6c02",light:Mxe[500],dark:Mxe[900]}}function yhr(n){return`oklch(from ${n} var(--__l) 0 h / var(--__a))`}function Ozt(n){const{mode:e="light",contrastThreshold:t=3,tonalOffset:i=.2,colorSpace:r,...o}=n,l=n.primary||phr(e),c=n.secondary||ghr(e),d=n.error||mhr(e),h=n.info||bhr(e),p=n.success||vhr(e),m=n.warning||whr(e);function b(T){return r?yhr(T):wdr(T,IOt.text.primary)>=t?IOt.text.primary:xci.text.primary}const w=({color:T,name:I,mainShade:D=500,lightShade:A=300,darkShade:M=700})=>{if(T={...T},!T.main&&T[D]&&(T.main=T[D]),!T.hasOwnProperty("main"))throw new Error(mW(11,I?` (${I})`:"",D));if(typeof T.main!="string")throw new Error(mW(12,I?` (${I})`:"",JSON.stringify(T.main)));return r?(zNn(r,T,"light",A,i),zNn(r,T,"dark",M,i)):($Nn(T,"light",A,i),$Nn(T,"dark",M,i)),T.contrastText||(T.contrastText=b(T.main)),T};let _;return e==="light"?_=Sci():e==="dark"&&(_=Eci()),F_({common:{...a6e},mode:e,primary:w({color:l,name:"primary"}),secondary:w({color:c,name:"secondary",mainShade:"A400",lightShade:"A200",darkShade:"A700"}),error:w({color:d,name:"error"}),warning:w({color:m,name:"warning"}),info:w({color:h,name:"info"}),success:w({color:p,name:"success"}),grey:DOt,contrastThreshold:t,getContrastText:b,augmentColor:w,tonalOffset:i,..._},o)}function _hr(n){const e={};return Object.entries(n).forEach(i=>{const[r,o]=i;typeof o=="object"&&(e[r]=`${o.fontStyle?`${o.fontStyle} `:""}${o.fontVariant?`${o.fontVariant} `:""}${o.fontWeight?`${o.fontWeight} `:""}${o.fontStretch?`${o.fontStretch} `:""}${o.fontSize||""}${o.lineHeight?`/${o.lineHeight} `:""}${o.fontFamily||""}`)}),e}function Chr(n,e){return{toolbar:{minHeight:56,[n.up("xs")]:{"@media (orientation: landscape)":{minHeight:48}},[n.up("sm")]:{minHeight:64}},...e}}function Shr(n){return Math.round(n*1e5)/1e5}const UNn={textTransform:"uppercase"},qNn='"Roboto", "Helvetica", "Arial", sans-serif';function kci(n,e){const{fontFamily:t=qNn,fontSize:i=14,fontWeightLight:r=300,fontWeightRegular:o=400,fontWeightMedium:l=500,fontWeightBold:c=700,htmlFontSize:d=16,allVariants:h,pxToRem:p,...m}=typeof e=="function"?e(n):e,b=i/14,w=p||(T=>`${T/d*b}rem`),_=(T,I,D,A,M)=>({fontFamily:t,fontWeight:T,fontSize:w(I),lineHeight:D,...t===qNn?{letterSpacing:`${Shr(A/I)}em`}:{},...M,...h}),x={h1:_(r,96,1.167,-1.5),h2:_(r,60,1.2,-.5),h3:_(o,48,1.167,0),h4:_(o,34,1.235,.25),h5:_(o,24,1.334,0),h6:_(l,20,1.6,.15),subtitle1:_(o,16,1.75,.15),subtitle2:_(l,14,1.57,.1),body1:_(o,16,1.5,.15),body2:_(o,14,1.43,.15),button:_(l,14,1.75,.4,UNn),caption:_(o,12,1.66,.4),overline:_(o,12,2.66,1,UNn),inherit:{fontFamily:"inherit",fontWeight:"inherit",fontSize:"inherit",lineHeight:"inherit",letterSpacing:"inherit"}};return F_({htmlFontSize:d,pxToRem:w,fontFamily:t,fontSize:i,fontWeightLight:r,fontWeightRegular:o,fontWeightMedium:l,fontWeightBold:c,...x},m,{clone:!1})}const xhr=.2,Ehr=.14,khr=.12;function C1(...n){return[`${n[0]}px ${n[1]}px ${n[2]}px ${n[3]}px rgba(0,0,0,${xhr})`,`${n[4]}px ${n[5]}px ${n[6]}px ${n[7]}px rgba(0,0,0,${Ehr})`,`${n[8]}px ${n[9]}px ${n[10]}px ${n[11]}px rgba(0,0,0,${khr})`].join(",")}const Thr=["none",C1(0,2,1,-1,0,1,1,0,0,1,3,0),C1(0,3,1,-2,0,2,2,0,0,1,5,0),C1(0,3,3,-2,0,3,4,0,0,1,8,0),C1(0,2,4,-1,0,4,5,0,0,1,10,0),C1(0,3,5,-1,0,5,8,0,0,1,14,0),C1(0,3,5,-1,0,6,10,0,0,1,18,0),C1(0,4,5,-2,0,7,10,1,0,2,16,1),C1(0,5,5,-3,0,8,10,1,0,3,14,2),C1(0,5,6,-3,0,9,12,1,0,3,16,2),C1(0,6,6,-3,0,10,14,1,0,4,18,3),C1(0,6,7,-4,0,11,15,1,0,4,20,3),C1(0,7,8,-4,0,12,17,2,0,5,22,4),C1(0,7,8,-4,0,13,19,2,0,5,24,4),C1(0,7,9,-4,0,14,21,2,0,5,26,4),C1(0,8,9,-5,0,15,22,2,0,6,28,5),C1(0,8,10,-5,0,16,24,2,0,6,30,5),C1(0,8,11,-5,0,17,26,2,0,6,32,5),C1(0,9,11,-5,0,18,28,2,0,7,34,6),C1(0,9,12,-6,0,19,29,2,0,7,36,6),C1(0,10,13,-6,0,20,31,3,0,8,38,7),C1(0,10,13,-6,0,21,33,3,0,8,40,7),C1(0,10,14,-6,0,22,35,3,0,8,42,7),C1(0,11,14,-7,0,23,36,3,0,9,44,8),C1(0,11,15,-7,0,24,38,3,0,9,46,8)],Lhr={easeInOut:"cubic-bezier(0.4, 0, 0.2, 1)",easeOut:"cubic-bezier(0.0, 0, 0.2, 1)",easeIn:"cubic-bezier(0.4, 0, 1, 1)",sharp:"cubic-bezier(0.4, 0, 0.6, 1)"},Tci={shortest:150,shorter:200,short:250,standard:300,complex:375,enteringScreen:225,leavingScreen:195};function GNn(n){return`${Math.round(n)}ms`}function Dhr(n){if(!n)return 0;const e=n/36;return Math.min(Math.round((4+15*e**.25+e/5)*10),3e3)}function Ihr(n){const e={...Lhr,...n.easing},t={...Tci,...n.duration};return{getAutoHeightDuration:Dhr,create:(r=["all"],o={})=>{const{duration:l=t.standard,easing:c=e.easeInOut,delay:d=0,...h}=o;return(Array.isArray(r)?r:[r]).map(p=>`${p} ${typeof l=="string"?l:GNn(l)} ${c} ${typeof d=="string"?d:GNn(d)}`).join(",")},...n,easing:e,duration:t}}const Ahr={mobileStepper:1e3,fab:1050,speedDial:1050,appBar:1100,drawer:1200,modal:1300,snackbar:1400,tooltip:1500};function Rhr(n){return ZP(n)||typeof n>"u"||typeof n=="string"||typeof n=="boolean"||typeof n=="number"||Array.isArray(n)}function Lci(n={}){const e={...n};function t(i){const r=Object.entries(i);for(let o=0;o{if(!Number.isNaN(+n))return+n;const e=n.match(/\d*\.?\d+/g);if(!e)return 0;let t=0;for(let i=0;iO_(_,x),w),w.unstable_sxConfig={...l5e,...p?.unstable_sxConfig},w.unstable_sx=function(x){return ZK({sx:x,theme:this})},w.toRuntimeSource=Eci,Lhr(w),w}function MOt(n){let e;return n<1?e=5.11916*n**2:e=4.5*Math.log(n+1)+2,Math.round(e*10)/1e3}const Dhr=[...Array(25)].map((n,e)=>{if(e===0)return"none";const t=MOt(e);return`linear-gradient(rgba(255 255 255 / ${t}), rgba(255 255 255 / ${t}))`});function kci(n){return{inputPlaceholder:n==="dark"?.5:.42,inputUnderline:n==="dark"?.7:.42,switchTrackDisabled:n==="dark"?.2:.12,switchTrack:n==="dark"?.3:.38}}function Tci(n){return n==="dark"?Dhr:[]}function Ihr(n){const{palette:e={mode:"light"},opacity:t,overlays:i,colorSpace:r,...o}=n,l=Pzt({...e,colorSpace:r});return{palette:l,opacity:{...kci(l.mode),...t},overlays:i||Tci(l.mode),...o}}function Ahr(n){return!!n[0].match(/(cssVarPrefix|colorSchemeSelector|modularCssLayers|rootSelector|typography|mixins|breakpoints|direction|transitions)/)||!!n[0].match(/sxConfig$/)||n[0]==="palette"&&!!n[1]?.match(/(mode|contrastThreshold|tonalOffset)/)}const Rhr=n=>[...[...Array(25)].map((e,t)=>`--${n?`${n}-`:""}overlays-${t}`),`--${n?`${n}-`:""}palette-AppBar-darkBg`,`--${n?`${n}-`:""}palette-AppBar-darkColor`],Mhr=n=>(e,t)=>{const i=n.rootSelector||":root",r=n.colorSchemeSelector;let o=r;if(r==="class"&&(o=".%s"),r==="data"&&(o="[data-%s]"),r?.startsWith("data-")&&!r.includes("%s")&&(o=`[${r}="%s"]`),n.defaultColorScheme===e){if(e==="dark"){const l={};return Rhr(n.cssVarPrefix).forEach(c=>{l[c]=t[c],delete t[c]}),o==="media"?{[i]:t,"@media (prefers-color-scheme: dark)":{[i]:l}}:o?{[o.replace("%s",e)]:l,[`${i}, ${o.replace("%s",e)}`]:t}:{[i]:{...t,...l}}}if(o&&o!=="media")return`${i}, ${o.replace("%s",String(e))}`}else if(e){if(o==="media")return{[`@media (prefers-color-scheme: ${String(e)})`]:{[i]:t}};if(o)return o.replace("%s",String(e))}return i};function Ohr(n,e){e.forEach(t=>{n[t]||(n[t]={})})}function er(n,e,t){!n[e]&&t&&(n[e]=t)}function lke(n){return typeof n!="string"||!n.startsWith("hsl")?n:pci(n)}function EH(n,e){`${e}Channel`in n||(n[`${e}Channel`]=ake(lke(n[e])))}function Nhr(n){return typeof n=="number"?`${n}px`:typeof n=="string"||typeof n=="function"||Array.isArray(n)?n:"8px"}const aP=n=>{try{return n()}catch{}},Phr=(n="mui")=>Odr(n);function DEt(n,e,t,i,r){if(!t)return;t=t===!0?{}:t;const o=r==="dark"?"dark":"light";if(!i){e[r]=Ihr({...t,palette:{mode:o,...t?.palette},colorSpace:n});return}const{palette:l,...c}=ROt({...i,palette:{mode:o,...t?.palette},colorSpace:n});return e[r]={...t,palette:l,opacity:{...kci(o),...t?.opacity},overlays:t?.overlays||Tci(o)},c}function Fhr(n={},...e){const{colorSchemes:t={light:!0},defaultColorScheme:i,disableCssColorScheme:r=!1,cssVarPrefix:o="mui",nativeColor:l=!1,shouldSkipGeneratingVar:c=Ahr,colorSchemeSelector:d=t.light&&t.dark?"media":void 0,rootSelector:h=":root",...p}=n,m=Object.keys(t)[0],b=i||(t.light&&m!=="light"?"light":m),w=Phr(o),{[b]:_,light:x,dark:T,...I}=t,L={...I};let A=_;if((b==="dark"&&!("dark"in t)||b==="light"&&!("light"in t))&&(A=!0),!A)throw new Error(bW(21,b));let M;l&&(M="oklch");const O=DEt(M,L,A,p,b);x&&!L.light&&DEt(M,L,x,void 0,"light"),T&&!L.dark&&DEt(M,L,T,void 0,"dark");let F={defaultColorScheme:b,...O,cssVarPrefix:o,colorSchemeSelector:d,rootSelector:h,getCssVar:w,colorSchemes:L,font:{...ghr(O.typography),...O.font},spacing:Nhr(p.spacing)};Object.keys(F.colorSchemes).forEach(ee=>{const G=F.colorSchemes[ee].palette,te=ie=>{const se=ie.split("-"),de=se[1],ne=se[2];return w(ie,G[de][ne])};G.mode==="light"&&(er(G.common,"background","#fff"),er(G.common,"onBackground","#000")),G.mode==="dark"&&(er(G.common,"background","#000"),er(G.common,"onBackground","#fff"));function Q(ie,se,de){if(M){let ne;return ie===hte&&(ne=`transparent ${((1-de)*100).toFixed(0)}%`),ie===qp&&(ne=`#000 ${(de*100).toFixed(0)}%`),ie===Gp&&(ne=`#fff ${(de*100).toFixed(0)}%`),`color-mix(in ${M}, ${se}, ${ne})`}return ie(se,de)}if(Ohr(G,["Alert","AppBar","Avatar","Button","Chip","FilledInput","LinearProgress","Skeleton","Slider","SnackbarContent","SpeedDialAction","StepConnector","StepContent","Switch","TableCell","Tooltip"]),G.mode==="light"){er(G.Alert,"errorColor",Q(qp,l?w("palette-error-light"):G.error.light,.6)),er(G.Alert,"infoColor",Q(qp,l?w("palette-info-light"):G.info.light,.6)),er(G.Alert,"successColor",Q(qp,l?w("palette-success-light"):G.success.light,.6)),er(G.Alert,"warningColor",Q(qp,l?w("palette-warning-light"):G.warning.light,.6)),er(G.Alert,"errorFilledBg",te("palette-error-main")),er(G.Alert,"infoFilledBg",te("palette-info-main")),er(G.Alert,"successFilledBg",te("palette-success-main")),er(G.Alert,"warningFilledBg",te("palette-warning-main")),er(G.Alert,"errorFilledColor",aP(()=>G.getContrastText(G.error.main))),er(G.Alert,"infoFilledColor",aP(()=>G.getContrastText(G.info.main))),er(G.Alert,"successFilledColor",aP(()=>G.getContrastText(G.success.main))),er(G.Alert,"warningFilledColor",aP(()=>G.getContrastText(G.warning.main))),er(G.Alert,"errorStandardBg",Q(Gp,l?w("palette-error-light"):G.error.light,.9)),er(G.Alert,"infoStandardBg",Q(Gp,l?w("palette-info-light"):G.info.light,.9)),er(G.Alert,"successStandardBg",Q(Gp,l?w("palette-success-light"):G.success.light,.9)),er(G.Alert,"warningStandardBg",Q(Gp,l?w("palette-warning-light"):G.warning.light,.9)),er(G.Alert,"errorIconColor",te("palette-error-main")),er(G.Alert,"infoIconColor",te("palette-info-main")),er(G.Alert,"successIconColor",te("palette-success-main")),er(G.Alert,"warningIconColor",te("palette-warning-main")),er(G.AppBar,"defaultBg",te("palette-grey-100")),er(G.Avatar,"defaultBg",te("palette-grey-400")),er(G.Button,"inheritContainedBg",te("palette-grey-300")),er(G.Button,"inheritContainedHoverBg",te("palette-grey-A100")),er(G.Chip,"defaultBorder",te("palette-grey-400")),er(G.Chip,"defaultAvatarColor",te("palette-grey-700")),er(G.Chip,"defaultIconColor",te("palette-grey-700")),er(G.FilledInput,"bg","rgba(0, 0, 0, 0.06)"),er(G.FilledInput,"hoverBg","rgba(0, 0, 0, 0.09)"),er(G.FilledInput,"disabledBg","rgba(0, 0, 0, 0.12)"),er(G.LinearProgress,"primaryBg",Q(Gp,l?w("palette-primary-main"):G.primary.main,.62)),er(G.LinearProgress,"secondaryBg",Q(Gp,l?w("palette-secondary-main"):G.secondary.main,.62)),er(G.LinearProgress,"errorBg",Q(Gp,l?w("palette-error-main"):G.error.main,.62)),er(G.LinearProgress,"infoBg",Q(Gp,l?w("palette-info-main"):G.info.main,.62)),er(G.LinearProgress,"successBg",Q(Gp,l?w("palette-success-main"):G.success.main,.62)),er(G.LinearProgress,"warningBg",Q(Gp,l?w("palette-warning-light"):G.warning.main,.62)),er(G.Skeleton,"bg",M?Q(hte,l?w("palette-text-primary"):G.text.primary,.11):`rgba(${te("palette-text-primaryChannel")} / 0.11)`),er(G.Slider,"primaryTrack",Q(Gp,l?w("palette-primary-main"):G.primary.main,.62)),er(G.Slider,"secondaryTrack",Q(Gp,l?w("palette-secondary-main"):G.secondary.main,.62)),er(G.Slider,"errorTrack",Q(Gp,l?w("palette-error-main"):G.error.main,.62)),er(G.Slider,"infoTrack",Q(Gp,l?w("palette-info-main"):G.info.main,.62)),er(G.Slider,"successTrack",Q(Gp,l?w("palette-success-main"):G.success.main,.62)),er(G.Slider,"warningTrack",Q(Gp,l?w("palette-warning-main"):G.warning.main,.62));const ie=M?Q(qp,l?w("palette-background-default"):G.background.default,.6825):EBe(G.background.default,.8);er(G.SnackbarContent,"bg",ie),er(G.SnackbarContent,"color",aP(()=>M?AOt.text.primary:G.getContrastText(ie))),er(G.SpeedDialAction,"fabHoverBg",EBe(G.background.paper,.15)),er(G.StepConnector,"border",te("palette-grey-400")),er(G.StepContent,"border",te("palette-grey-400")),er(G.Switch,"defaultColor",te("palette-common-white")),er(G.Switch,"defaultDisabledColor",te("palette-grey-100")),er(G.Switch,"primaryDisabledColor",Q(Gp,l?w("palette-primary-main"):G.primary.main,.62)),er(G.Switch,"secondaryDisabledColor",Q(Gp,l?w("palette-secondary-main"):G.secondary.main,.62)),er(G.Switch,"errorDisabledColor",Q(Gp,l?w("palette-error-main"):G.error.main,.62)),er(G.Switch,"infoDisabledColor",Q(Gp,l?w("palette-info-main"):G.info.main,.62)),er(G.Switch,"successDisabledColor",Q(Gp,l?w("palette-success-main"):G.success.main,.62)),er(G.Switch,"warningDisabledColor",Q(Gp,l?w("palette-warning-main"):G.warning.main,.62)),er(G.TableCell,"border",Q(Gp,hte(l?w("palette-divider"):G.divider,1),.88)),er(G.Tooltip,"bg",Q(hte,l?w("palette-grey-700"):G.grey[700],.92))}if(G.mode==="dark"){er(G.Alert,"errorColor",Q(Gp,l?w("palette-error-light"):G.error.light,.6)),er(G.Alert,"infoColor",Q(Gp,l?w("palette-info-light"):G.info.light,.6)),er(G.Alert,"successColor",Q(Gp,l?w("palette-success-light"):G.success.light,.6)),er(G.Alert,"warningColor",Q(Gp,l?w("palette-warning-light"):G.warning.light,.6)),er(G.Alert,"errorFilledBg",te("palette-error-dark")),er(G.Alert,"infoFilledBg",te("palette-info-dark")),er(G.Alert,"successFilledBg",te("palette-success-dark")),er(G.Alert,"warningFilledBg",te("palette-warning-dark")),er(G.Alert,"errorFilledColor",aP(()=>G.getContrastText(G.error.dark))),er(G.Alert,"infoFilledColor",aP(()=>G.getContrastText(G.info.dark))),er(G.Alert,"successFilledColor",aP(()=>G.getContrastText(G.success.dark))),er(G.Alert,"warningFilledColor",aP(()=>G.getContrastText(G.warning.dark))),er(G.Alert,"errorStandardBg",Q(qp,l?w("palette-error-light"):G.error.light,.9)),er(G.Alert,"infoStandardBg",Q(qp,l?w("palette-info-light"):G.info.light,.9)),er(G.Alert,"successStandardBg",Q(qp,l?w("palette-success-light"):G.success.light,.9)),er(G.Alert,"warningStandardBg",Q(qp,l?w("palette-warning-light"):G.warning.light,.9)),er(G.Alert,"errorIconColor",te("palette-error-main")),er(G.Alert,"infoIconColor",te("palette-info-main")),er(G.Alert,"successIconColor",te("palette-success-main")),er(G.Alert,"warningIconColor",te("palette-warning-main")),er(G.AppBar,"defaultBg",te("palette-grey-900")),er(G.AppBar,"darkBg",te("palette-background-paper")),er(G.AppBar,"darkColor",te("palette-text-primary")),er(G.Avatar,"defaultBg",te("palette-grey-600")),er(G.Button,"inheritContainedBg",te("palette-grey-800")),er(G.Button,"inheritContainedHoverBg",te("palette-grey-700")),er(G.Chip,"defaultBorder",te("palette-grey-700")),er(G.Chip,"defaultAvatarColor",te("palette-grey-300")),er(G.Chip,"defaultIconColor",te("palette-grey-300")),er(G.FilledInput,"bg","rgba(255, 255, 255, 0.09)"),er(G.FilledInput,"hoverBg","rgba(255, 255, 255, 0.13)"),er(G.FilledInput,"disabledBg","rgba(255, 255, 255, 0.12)"),er(G.LinearProgress,"primaryBg",Q(qp,l?w("palette-primary-main"):G.primary.main,.5)),er(G.LinearProgress,"secondaryBg",Q(qp,l?w("palette-secondary-main"):G.secondary.main,.5)),er(G.LinearProgress,"errorBg",Q(qp,l?w("palette-error-main"):G.error.main,.5)),er(G.LinearProgress,"infoBg",Q(qp,l?w("palette-info-main"):G.info.main,.5)),er(G.LinearProgress,"successBg",Q(qp,l?w("palette-success-main"):G.success.main,.5)),er(G.LinearProgress,"warningBg",Q(qp,l?w("palette-warning-main"):G.warning.main,.5)),er(G.Skeleton,"bg",M?Q(hte,l?w("palette-text-primary"):G.text.primary,.13):`rgba(${te("palette-text-primaryChannel")} / 0.13)`),er(G.Slider,"primaryTrack",Q(qp,l?w("palette-primary-main"):G.primary.main,.5)),er(G.Slider,"secondaryTrack",Q(qp,l?w("palette-secondary-main"):G.secondary.main,.5)),er(G.Slider,"errorTrack",Q(qp,l?w("palette-error-main"):G.error.main,.5)),er(G.Slider,"infoTrack",Q(qp,l?w("palette-info-main"):G.info.main,.5)),er(G.Slider,"successTrack",Q(qp,l?w("palette-success-main"):G.success.main,.5)),er(G.Slider,"warningTrack",Q(qp,l?w("palette-warning-light"):G.warning.main,.5));const ie=M?Q(Gp,l?w("palette-background-default"):G.background.default,.985):EBe(G.background.default,.98);er(G.SnackbarContent,"bg",ie),er(G.SnackbarContent,"color",aP(()=>M?_ci.text.primary:G.getContrastText(ie))),er(G.SpeedDialAction,"fabHoverBg",EBe(G.background.paper,.15)),er(G.StepConnector,"border",te("palette-grey-600")),er(G.StepContent,"border",te("palette-grey-600")),er(G.Switch,"defaultColor",te("palette-grey-300")),er(G.Switch,"defaultDisabledColor",te("palette-grey-600")),er(G.Switch,"primaryDisabledColor",Q(qp,l?w("palette-primary-main"):G.primary.main,.55)),er(G.Switch,"secondaryDisabledColor",Q(qp,l?w("palette-secondary-main"):G.secondary.main,.55)),er(G.Switch,"errorDisabledColor",Q(qp,l?w("palette-error-main"):G.error.main,.55)),er(G.Switch,"infoDisabledColor",Q(qp,l?w("palette-info-main"):G.info.main,.55)),er(G.Switch,"successDisabledColor",Q(qp,l?w("palette-success-main"):G.success.main,.55)),er(G.Switch,"warningDisabledColor",Q(qp,l?w("palette-warning-light"):G.warning.main,.55)),er(G.TableCell,"border",Q(qp,hte(l?w("palette-divider"):G.divider,1),.68)),er(G.Tooltip,"bg",Q(hte,l?w("palette-grey-700"):G.grey[700],.92))}EH(G.background,"default"),EH(G.background,"paper"),EH(G.common,"background"),EH(G.common,"onBackground"),EH(G,"divider"),Object.keys(G).forEach(ie=>{const se=G[ie];ie!=="tonalOffset"&&se&&typeof se=="object"&&(se.main&&er(G[ie],"mainChannel",ake(lke(se.main))),se.light&&er(G[ie],"lightChannel",ake(lke(se.light))),se.dark&&er(G[ie],"darkChannel",ake(lke(se.dark))),se.contrastText&&er(G[ie],"contrastTextChannel",ake(lke(se.contrastText))),ie==="text"&&(EH(G[ie],"primary"),EH(G[ie],"secondary")),ie==="action"&&(se.active&&EH(G[ie],"active"),se.selected&&EH(G[ie],"selected")))})}),F=e.reduce((ee,G)=>O_(ee,G),F);const j={prefix:o,disableCssColorScheme:r,shouldSkipGeneratingVar:c,getSelector:Mhr(F),enableContrastVars:l},{vars:W,generateThemeVars:q,generateStyleSheets:Z}=Fdr(F,j);return F.vars=W,Object.entries(F.colorSchemes[F.defaultColorScheme]).forEach(([ee,G])=>{F[ee]=G}),F.generateThemeVars=q,F.generateStyleSheets=Z,F.generateSpacing=function(){return rci(p.spacing,_et(this))},F.getColorSchemeSelector=jdr(d),F.spacing=F.generateSpacing(),F.shouldSkipGeneratingVar=c,F.unstable_sxConfig={...l5e,...p?.unstable_sxConfig},F.unstable_sx=function(G){return ZK({sx:G,theme:this})},F.toRuntimeSource=Eci,F}function KNn(n,e,t){n.colorSchemes&&t&&(n.colorSchemes[e]={...t!==!0&&t,palette:Pzt({...t===!0?{}:t.palette,mode:e})})}function G1e(n={},...e){const{palette:t,cssVariables:i=!1,colorSchemes:r=t?void 0:{light:!0},defaultColorScheme:o=t?.mode,...l}=n,c=o||"light",d=r?.[c],h={...r,...t?{[c]:{...typeof d!="boolean"&&d,palette:t}}:void 0};if(i===!1){if(!("colorSchemes"in n))return ROt(n,...e);let p=t;"palette"in n||h[c]&&(h[c]!==!0?p=h[c].palette:c==="dark"&&(p={mode:"dark"}));const m=ROt({...n,palette:p},...e);return m.defaultColorScheme=c,m.colorSchemes=h,m.palette.mode==="light"&&(m.colorSchemes.light={...h.light!==!0&&h.light,palette:m.palette},KNn(m,"dark",h.dark)),m.palette.mode==="dark"&&(m.colorSchemes.dark={...h.dark!==!0&&h.dark,palette:m.palette},KNn(m,"light",h.light)),m}return!t&&!("light"in h)&&c==="light"&&(h.light=!0),Fhr({...l,colorSchemes:h,defaultColorScheme:c,...typeof i!="boolean"&&i},...e)}const Fet=G1e(),w3="$$material";function Lf(){const n=aoe(Fet);return n[w3]||n}function jhr(n){return k.jsx(sci,{...n,defaultTheme:Fet,themeId:w3})}function jet(n){return n!=="ownerState"&&n!=="theme"&&n!=="sx"&&n!=="as"}const B_=n=>jet(n)&&n!=="classes",tn=uci({themeId:w3,defaultTheme:Fet,rootShouldForwardProp:B_});function Fzt(n){return function(t){return k.jsx(jhr,{styles:typeof n=="function"?i=>n({theme:i,...t}):n})}}function Hhr(){return Let}function Wo(n){return _dr(n)}const OOt=typeof Fzt({})=="function",Bhr=(n,e)=>({WebkitFontSmoothing:"antialiased",MozOsxFontSmoothing:"grayscale",boxSizing:"border-box",WebkitTextSizeAdjust:"100%",...e&&!n.vars&&{colorScheme:n.palette.mode}}),Whr=n=>({color:(n.vars||n).palette.text.primary,...n.typography.body1,backgroundColor:(n.vars||n).palette.background.default,"@media print":{backgroundColor:(n.vars||n).palette.common.white}}),Lci=(n,e=!1)=>{const t={};e&&n.colorSchemes&&typeof n.getColorSchemeSelector=="function"&&Object.entries(n.colorSchemes).forEach(([o,l])=>{const c=n.getColorSchemeSelector(o);c.startsWith("@")?t[c]={":root":{colorScheme:l.palette?.mode}}:t[c.replace(/\s*&/,"")]={colorScheme:l.palette?.mode}});let i={html:Bhr(n,e),"*, *::before, *::after":{boxSizing:"inherit"},"strong, b":{fontWeight:n.typography.fontWeightBold},body:{margin:0,...Whr(n),"&::backdrop":{backgroundColor:(n.vars||n).palette.background.default}},...t};const r=n.components?.MuiCssBaseline?.styleOverrides;return r&&(i=[i,r]),i},gUe="mui-ecs",Vhr=n=>{const e=Lci(n,!1),t=Array.isArray(e)?e[0]:e;return!n.vars&&t&&(t.html[`:root:has(${gUe})`]={colorScheme:n.palette.mode}),n.colorSchemes&&Object.entries(n.colorSchemes).forEach(([i,r])=>{const o=n.getColorSchemeSelector(i);o.startsWith("@")?t[o]={[`:root:not(:has(.${gUe}))`]:{colorScheme:r.palette?.mode}}:t[o.replace(/\s*&/,"")]={[`&:not(:has(.${gUe}))`]:{colorScheme:r.palette?.mode}}}),e},$hr=Fzt(OOt?({theme:n,enableColorScheme:e})=>Lci(n,e):({theme:n})=>Vhr(n));function zhr(n){const e=Wo({props:n,name:"MuiCssBaseline"}),{children:t,enableColorScheme:i=!1}=e;return k.jsxs(D.Fragment,{children:[OOt&&k.jsx($hr,{enableColorScheme:i}),!OOt&&!i&&k.jsx("span",{className:gUe,style:{display:"none"}}),t]})}var Ws=function(){return Ws=Object.assign||function(e){for(var t,i=1,r=arguments.length;i=n.length&&(n=void 0),{value:n&&n[i++],done:!n}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")}function xf(n,e){var t=typeof Symbol=="function"&&n[Symbol.iterator];if(!t)return n;var i=t.call(n),r,o=[],l;try{for(;(e===void 0||e-- >0)&&!(r=i.next()).done;)o.push(r.value)}catch(c){l={error:c}}finally{try{r&&!r.done&&(t=i.return)&&t.call(i)}finally{if(l)throw l.error}}return o}function $m(n,e,t){if(arguments.length===2)for(var i=0,r=e.length,o;i"u"||typeof i=="string"||fme(i)?{target:i,event:n}:Ws(Ws({},i),{event:n})});return t}function lfr(n){if(!(n===void 0||n===Zhr))return AR(n)}function Fci(n,e,t,i,r){var o=n.options.guards,l={state:r,cond:e,_event:i};if(e.type===FOt)return(o?.[e.name]||e.predicate)(t,i.data,l);var c=o?.[e.type];if(!c)throw new Error("Guard '".concat(e.type,"' is not implemented on machine '").concat(n.id,"'."));return c(t,i.data,l)}function jci(n){return typeof n=="string"?{type:n}:n}function r4e(n,e,t){var i=function(){},r=typeof n=="object",o=r?n:null;return{next:((r?n.next:n)||i).bind(o),error:((r?n.error:e)||i).bind(o),complete:((r?n.complete:t)||i).bind(o)}}function TBe(n,e){return"".concat(n,":invocation[").concat(e,"]")}function HOt(n){return(n.type===f5e||n.type===Het&&n.to===Jre.Internal)&&typeof n.delay!="number"}var Qpe=H2({type:qhr});function BOt(n,e){return e&&e[n]||void 0}function uLe(n,e){var t;if(qf(n)||typeof n=="number"){var i=BOt(n,e);Zf(i)?t={type:n,exec:i}:i?t=i:t={type:n,exec:void 0}}else if(Zf(n))t={type:n.name||n.toString(),exec:n};else{var i=BOt(n.type,e);if(Zf(i))t=Ws(Ws({},n),{exec:i});else if(i){var r=i.type||n.type;t=Ws(Ws(Ws({},i),n),{type:r})}else t=n}return t}var wG=function(n,e){if(!n)return[];var t=K1e(n)?n:[n];return t.map(function(i){return uLe(i,e)})};function Vzt(n){var e=uLe(n);return Ws(Ws({id:qf(n)?n:e.id},e),{type:e.type})}function Hci(n,e){return{type:f5e,event:typeof n=="function"?n:Bet(n),delay:e?e.delay:void 0,id:e?.id}}function cfr(n,e,t,i){var r={_event:t},o=H2(Zf(n.event)?n.event(e,t.data,r):n.event),l;if(qf(n.delay)){var c=i&&i[n.delay];l=Zf(c)?c(e,t.data,r):c}else l=Zf(n.delay)?n.delay(e,t.data,r):n.delay;return Ws(Ws({},n),{type:f5e,_event:o,delay:l})}function p5e(n,e){return{to:e?e.to:void 0,type:Het,event:Zf(n)?n:Bet(n),delay:e?e.delay:void 0,id:e&&e.id!==void 0?e.id:Zf(n)?n.name:Mci(n)}}function ufr(n,e,t,i){var r={_event:t},o=H2(Zf(n.event)?n.event(e,t.data,r):n.event),l;if(qf(n.delay)){var c=i&&i[n.delay];l=Zf(c)?c(e,t.data,r):c}else l=Zf(n.delay)?n.delay(e,t.data,r):n.delay;var d=Zf(n.to)?n.to(e,t.data,r):n.to;return Ws(Ws({},n),{to:d,_event:o,event:o.data,delay:l})}function dfr(n,e){return p5e(n,Ws(Ws({},e),{to:Jre.Parent}))}function hfr(n,e,t){return p5e(e,Ws(Ws({},t),{to:n}))}var ffr=function(n,e,t){return Ws(Ws({},n),{value:qf(n.expr)?n.expr:n.expr(e,t.data,{_event:t})})},pfr=function(n){return{type:Dci,sendId:n}};function gfr(n){var e=Vzt(n);return{type:Ep.Start,activity:e,exec:void 0}}function mfr(n){var e=Zf(n)?n:Vzt(n);return{type:Ep.Stop,activity:e,exec:void 0}}function bfr(n,e,t){var i=Zf(n.activity)?n.activity(e,t.data):n.activity,r=typeof i=="string"?{id:i}:i,o={type:Ep.Stop,activity:r};return o}var vfr=function(n){return{type:Bzt,assignment:n}};function wfr(n,e){var t=e?"#".concat(e):"";return"".concat(Ep.After,"(").concat(n,")").concat(t)}function LBe(n,e){var t="".concat(Ep.DoneState,".").concat(n),i={type:t,data:e};return i.toString=function(){return t},i}function bUe(n,e){var t="".concat(Ep.DoneInvoke,".").concat(n),i={type:t,data:e};return i.toString=function(){return t},i}function uke(n,e){var t="".concat(Ep.ErrorPlatform,".").concat(n),i={type:t,data:e};return i.toString=function(){return t},i}function yfr(n){return{type:Ep.Pure,get:n}}function _fr(n,e){return p5e(function(t,i){return i},Ws(Ws({},e),{to:n}))}function Cfr(n){return{type:Ep.Choose,conds:n}}var Sfr=function(n){var e,t,i=[];try{for(var r=yh(n),o=r.next();!o.done;o=r.next())for(var l=o.value,c=0;c0;){var h=r.shift();t=n.transition(t,h,d),i.forEach(function(p){return p.next(t)})}o=!1}},c=Lfr({id:e.id,send:function(h){r.push(h),l()},getSnapshot:function(){return t},subscribe:function(h,p,m){var b=r4e(h,p,m);return i.add(b),b.next(t),{unsubscribe:function(){i.delete(b)}}}}),d={parent:e.parent,self:c,id:e.id||"anonymous",observers:i};return t=n.start?n.start(d):t,c}var VOt={sync:!1,autoForward:!1},ib;(function(n){n[n.NotStarted=0]="NotStarted",n[n.Running=1]="Running",n[n.Stopped=2]="Stopped"})(ib||(ib={}));var Hfr=(function(){function n(e,t){t===void 0&&(t=n.defaultOptions);var i=this;this.machine=e,this.delayedEventsMap={},this.listeners=new Set,this.contextListeners=new Set,this.stopListeners=new Set,this.doneListeners=new Set,this.eventListeners=new Set,this.sendListeners=new Set,this.initialized=!1,this.status=ib.NotStarted,this.children=new Map,this.forwardTo=new Set,this._outgoingQueue=[],this.init=this.start,this.send=function(p,m){if(K1e(p))return i.batch(p),i.state;var b=H2(Bet(p,m));if(i.status===ib.Stopped)return i.state;if(i.status!==ib.Running&&!i.options.deferEvents)throw new Error('Event "'.concat(b.name,'" was sent to uninitialized service "').concat(i.machine.id,`". Make sure .start() is called for this service, or set { deferEvents: true } in the service options. -Event: `).concat(JSON.stringify(b.data)));return i.scheduler.schedule(function(){i.forward(b);var w=i._nextState(b);i.update(w,b)}),i._state},this.sendTo=function(p,m,b){var w=i.parent&&(m===Jre.Parent||i.parent.id===m),_=w?i.parent:qf(m)?m===Jre.Internal?i:i.children.get(m)||Pxe.get(m):ofr(m)?m:void 0;if(!_){if(!w)throw new Error("Unable to send event to child '".concat(m,"' from service '").concat(i.id,"'."));return}if("machine"in _){if(i.status!==ib.Stopped||i.parent!==_||i.state.done){var x=Ws(Ws({},p),{name:p.name===Ghr?"".concat(uke(i.id)):p.name,origin:i.sessionId});!b&&i.machine.config.predictableActionArguments?i._outgoingQueue.push([_,x]):_.send(x)}}else!b&&i.machine.config.predictableActionArguments?i._outgoingQueue.push([_,p.data]):_.send(p.data)},this._exec=function(p,m,b,w){w===void 0&&(w=i.machine.options.actions);var _=p.exec||BOt(p.type,w),x=Zf(_)?_:_?_.exec:p.exec;if(x)try{return x(m,b.data,i.machine.config.predictableActionArguments?{action:p,_event:b}:{action:p,state:i.state,_event:b})}catch(Q){throw i.parent&&i.parent.send({type:"xstate.error",data:Q}),Q}switch(p.type){case f5e:{var T=p;i.defer(T);break}case Het:var I=p;if(typeof I.delay=="number"){i.defer(I);return}else I.to?i.sendTo(I._event,I.to,b===Qpe):i.send(I._event);break;case Dci:i.cancel(p.sendId);break;case NOt:{if(i.status!==ib.Running)return;var L=p.activity;if(!i.machine.config.predictableActionArguments&&!i.state.activities[L.id||L.type])break;if(L.type===Ep.Invoke){var A=jci(L.src),M=i.machine.options.services?i.machine.options.services[A.type]:void 0,O=L.id,F=L.data,j="autoForward"in L?L.autoForward:!!L.forward;if(!M)return;var W=F?Xqe(F,m,b):void 0;if(typeof M=="string")return;var q=Zf(M)?M(m,b.data,{data:W,src:A,meta:L.meta}):M;if(!q)return;var Z=void 0;fme(q)&&(q=W?q.withContext(W):q,Z={autoForward:j}),i.spawn(q,O,Z)}else i.spawnActivity(L);break}case Hzt:{i.stopChild(p.activity.id);break}case Ici:var ee=p,G=ee.label,te=ee.value;G?i.logger(G,te):i.logger(te);break}};var r=Ws(Ws({},n.defaultOptions),t),o=r.clock,l=r.logger,c=r.parent,d=r.id,h=d!==void 0?d:e.id;this.id=h,this.logger=l,this.clock=o,this.parent=c,this.options=r,this.scheduler=new tPn({deferEvents:this.options.deferEvents}),this.sessionId=Pxe.bookId()}return Object.defineProperty(n.prototype,"initialState",{get:function(){var e=this;return this._initialState?this._initialState:pfe(this,function(){return e._initialState=e.machine.initialState,e._initialState})},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"state",{get:function(){return this._state},enumerable:!1,configurable:!0}),n.prototype.execute=function(e,t){var i,r;try{for(var o=yh(e.actions),l=o.next();!l.done;l=o.next()){var c=l.value;this.exec(c,e,t)}}catch(d){i={error:d}}finally{try{l&&!l.done&&(r=o.return)&&r.call(o)}finally{if(i)throw i.error}}},n.prototype.update=function(e,t){var i,r,o,l,c,d,h,p,m=this;if(e._sessionid=this.sessionId,this._state=e,(!this.machine.config.predictableActionArguments||t===Qpe)&&this.options.execute)this.execute(this.state);else for(var b=void 0;b=this._outgoingQueue.shift();)b[0].send(b[1]);if(this.children.forEach(function(q){m.state.children[q.id]=q}),this.devTools&&this.devTools.send(t.data,e),e.event)try{for(var w=yh(this.eventListeners),_=w.next();!_.done;_=w.next()){var x=_.value;x(e.event)}}catch(q){i={error:q}}finally{try{_&&!_.done&&(r=w.return)&&r.call(w)}finally{if(i)throw i.error}}try{for(var T=yh(this.listeners),I=T.next();!I.done;I=T.next()){var x=I.value;x(e,e.event)}}catch(q){o={error:q}}finally{try{I&&!I.done&&(l=T.return)&&l.call(T)}finally{if(o)throw o.error}}try{for(var L=yh(this.contextListeners),A=L.next();!A.done;A=L.next()){var M=A.value;M(this.state.context,this.state.history?this.state.history.context:void 0)}}catch(q){c={error:q}}finally{try{A&&!A.done&&(d=L.return)&&d.call(L)}finally{if(c)throw c.error}}if(this.state.done){var O=e.configuration.find(function(q){return q.type==="final"&&q.parent===m.machine}),F=O&&O.doneData?Xqe(O.doneData,e.context,t):void 0;this._doneEvent=bUe(this.id,F);try{for(var j=yh(this.doneListeners),W=j.next();!W.done;W=j.next()){var x=W.value;x(this._doneEvent)}}catch(q){h={error:q}}finally{try{W&&!W.done&&(p=j.return)&&p.call(j)}finally{if(h)throw h.error}}this._stop(),this._stopChildren(),Pxe.free(this.sessionId)}},n.prototype.onTransition=function(e){return this.listeners.add(e),this.status===ib.Running&&e(this.state,this.state.event),this},n.prototype.subscribe=function(e,t,i){var r=this,o=r4e(e,t,i);this.listeners.add(o.next),this.status!==ib.NotStarted&&o.next(this.state);var l=function(){r.doneListeners.delete(l),r.stopListeners.delete(l),o.complete()};return this.status===ib.Stopped?o.complete():(this.onDone(l),this.onStop(l)),{unsubscribe:function(){r.listeners.delete(o.next),r.doneListeners.delete(l),r.stopListeners.delete(l)}}},n.prototype.onEvent=function(e){return this.eventListeners.add(e),this},n.prototype.onSend=function(e){return this.sendListeners.add(e),this},n.prototype.onChange=function(e){return this.contextListeners.add(e),this},n.prototype.onStop=function(e){return this.stopListeners.add(e),this},n.prototype.onDone=function(e){return this.status===ib.Stopped&&this._doneEvent?e(this._doneEvent):this.doneListeners.add(e),this},n.prototype.off=function(e){return this.listeners.delete(e),this.eventListeners.delete(e),this.sendListeners.delete(e),this.stopListeners.delete(e),this.doneListeners.delete(e),this.contextListeners.delete(e),this},n.prototype.start=function(e){var t=this;if(this.status===ib.Running)return this;this.machine._init(),Pxe.register(this.sessionId,this),this.initialized=!0,this.status=ib.Running;var i=e===void 0?this.initialState:pfe(this,function(){return Rfr(e)?t.machine.resolveState(e):t.machine.resolveState(f6.from(e,t.machine.context))});return this.options.devTools&&this.attachDev(),this.scheduler.initialize(function(){t.update(i,Qpe)}),this},n.prototype._stopChildren=function(){this.children.forEach(function(e){Zf(e.stop)&&e.stop()}),this.children.clear()},n.prototype._stop=function(){var e,t,i,r,o,l,c,d,h,p;try{for(var m=yh(this.listeners),b=m.next();!b.done;b=m.next()){var w=b.value;this.listeners.delete(w)}}catch(j){e={error:j}}finally{try{b&&!b.done&&(t=m.return)&&t.call(m)}finally{if(e)throw e.error}}try{for(var _=yh(this.stopListeners),x=_.next();!x.done;x=_.next()){var w=x.value;w(),this.stopListeners.delete(w)}}catch(j){i={error:j}}finally{try{x&&!x.done&&(r=_.return)&&r.call(_)}finally{if(i)throw i.error}}try{for(var T=yh(this.contextListeners),I=T.next();!I.done;I=T.next()){var w=I.value;this.contextListeners.delete(w)}}catch(j){o={error:j}}finally{try{I&&!I.done&&(l=T.return)&&l.call(T)}finally{if(o)throw o.error}}try{for(var L=yh(this.doneListeners),A=L.next();!A.done;A=L.next()){var w=A.value;this.doneListeners.delete(w)}}catch(j){c={error:j}}finally{try{A&&!A.done&&(d=L.return)&&d.call(L)}finally{if(c)throw c.error}}if(!this.initialized)return this;this.initialized=!1,this.status=ib.Stopped,this._initialState=void 0;try{for(var M=yh(Object.keys(this.delayedEventsMap)),O=M.next();!O.done;O=M.next()){var F=O.value;this.clock.clearTimeout(this.delayedEventsMap[F])}}catch(j){h={error:j}}finally{try{O&&!O.done&&(p=M.return)&&p.call(M)}finally{if(h)throw h.error}}this.scheduler.clear(),this.scheduler=new tPn({deferEvents:this.options.deferEvents})},n.prototype.stop=function(){var e=this,t=this.scheduler;return this._stop(),t.schedule(function(){var i;if(!(!((i=e._state)===null||i===void 0)&&i.done)){var r=H2({type:"xstate.stop"}),o=pfe(e,function(){var l=h0($m([],xf(e.state.configuration),!1).sort(function(m,b){return b.order-m.order}).map(function(m){return wG(m.onExit,e.machine.options.actions)})),c=xf(Qqe(e.machine,e.state,e.state.context,r,[{type:"exit",actions:l}],e.machine.config.predictableActionArguments?e._exec:void 0,e.machine.config.predictableActionArguments||e.machine.config.preserveActionOrder),2),d=c[0],h=c[1],p=new f6({value:e.state.value,context:h,_event:r,_sessionid:e.sessionId,historyValue:void 0,history:e.state,actions:d.filter(function(m){return!HOt(m)}),activities:{},events:[],configuration:[],transitions:[],children:{},done:e.state.done,tags:e.state.tags,machine:e.machine});return p.changed=!0,p});e.update(o,r),e._stopChildren(),Pxe.free(e.sessionId)}}),this},n.prototype.batch=function(e){var t=this;if(!(this.status===ib.NotStarted&&this.options.deferEvents)){if(this.status!==ib.Running)throw new Error("".concat(e.length,' event(s) were sent to uninitialized service "').concat(this.machine.id,'". Make sure .start() is called for this service, or set { deferEvents: true } in the service options.'))}if(e.length){var i=!!this.machine.config.predictableActionArguments&&this._exec;this.scheduler.schedule(function(){var r,o,l=t.state,c=!1,d=[],h=function(w){var _=H2(w);t.forward(_),l=pfe(t,function(){return t.machine.transition(l,_,void 0,i||void 0)}),d.push.apply(d,$m([],xf(t.machine.config.predictableActionArguments?l.actions:l.actions.map(function(x){return Mfr(x,l)})),!1)),c=c||!!l.changed};try{for(var p=yh(e),m=p.next();!m.done;m=p.next()){var b=m.value;h(b)}}catch(w){r={error:w}}finally{try{m&&!m.done&&(o=p.return)&&o.call(p)}finally{if(r)throw r.error}}l.changed=c,l.actions=d,t.update(l,H2(e[e.length-1]))})}},n.prototype.sender=function(e){return this.send.bind(this,e)},n.prototype._nextState=function(e,t){var i=this;t===void 0&&(t=!!this.machine.config.predictableActionArguments&&this._exec);var r=H2(e);if(r.name.indexOf(YNn)===0&&!this.state.nextEvents.some(function(l){return l.indexOf(YNn)===0}))throw r.data.data;var o=pfe(this,function(){return i.machine.transition(i.state,r,void 0,t||void 0)});return o},n.prototype.nextState=function(e){return this._nextState(e,!1)},n.prototype.forward=function(e){var t,i;try{for(var r=yh(this.forwardTo),o=r.next();!o.done;o=r.next()){var l=o.value,c=this.children.get(l);if(!c)throw new Error("Unable to forward event '".concat(e,"' from interpreter '").concat(this.id,"' to nonexistant child '").concat(l,"'."));c.send(e)}}catch(d){t={error:d}}finally{try{o&&!o.done&&(i=r.return)&&i.call(r)}finally{if(t)throw t.error}}},n.prototype.defer=function(e){var t=this,i=this.clock.setTimeout(function(){"to"in e&&e.to?t.sendTo(e._event,e.to,!0):t.send(e._event)},e.delay);e.id&&(this.delayedEventsMap[e.id]=i)},n.prototype.cancel=function(e){this.clock.clearTimeout(this.delayedEventsMap[e]),delete this.delayedEventsMap[e]},n.prototype.exec=function(e,t,i){i===void 0&&(i=this.machine.options.actions),this._exec(e,t.context,t._event,i)},n.prototype.removeChild=function(e){var t;this.children.delete(e),this.forwardTo.delete(e),(t=this.state)===null||t===void 0||delete t.children[e]},n.prototype.stopChild=function(e){var t=this.children.get(e);t&&(this.removeChild(e),Zf(t.stop)&&t.stop())},n.prototype.spawn=function(e,t,i){if(this.status!==ib.Running)return $zt(e,t);if(QNn(e))return this.spawnPromise(Promise.resolve(e),t);if(Zf(e))return this.spawnCallback(e,t);if(Tfr(e))return this.spawnActor(e,t);if(sfr(e))return this.spawnObservable(e,t);if(fme(e))return this.spawnMachine(e,Ws(Ws({},i),{id:t}));if(tfr(e))return this.spawnBehavior(e,t);throw new Error('Unable to spawn entity "'.concat(t,'" of type "').concat(typeof e,'".'))},n.prototype.spawnMachine=function(e,t){var i=this;t===void 0&&(t={});var r=new n(e,Ws(Ws({},this.options),{parent:this,id:t.id||e.id})),o=Ws(Ws({},VOt),t);o.sync&&r.onTransition(function(c){i.send(Aci,{state:c,id:r.id})});var l=r;return this.children.set(r.id,l),o.autoForward&&this.forwardTo.add(r.id),r.onDone(function(c){i.removeChild(r.id),i.send(H2(c,{origin:r.id}))}).start(),l},n.prototype.spawnBehavior=function(e,t){var i=jfr(e,{id:t,parent:this});return this.children.set(t,i),i},n.prototype.spawnPromise=function(e,t){var i,r=this,o=!1,l;e.then(function(d){o||(l=d,r.removeChild(t),r.send(H2(bUe(t,d),{origin:t})))},function(d){if(!o){r.removeChild(t);var h=uke(t,d);try{r.send(H2(h,{origin:t}))}catch{r.devTools&&r.devTools.send(h,r.state),r.machine.strict&&r.stop()}}});var c=(i={id:t,send:function(){},subscribe:function(d,h,p){var m=r4e(d,h,p),b=!1;return e.then(function(w){b||(m.next(w),!b&&m.complete())},function(w){b||m.error(w)}),{unsubscribe:function(){return b=!0}}},stop:function(){o=!0},toJSON:function(){return{id:t}},getSnapshot:function(){return l}},i[vG]=function(){return this},i);return this.children.set(t,c),c},n.prototype.spawnCallback=function(e,t){var i,r=this,o=!1,l=new Set,c=new Set,d,h=function(b){d=b,c.forEach(function(w){return w(b)}),!o&&r.send(H2(b,{origin:t}))},p;try{p=e(h,function(b){l.add(b)})}catch(b){this.send(uke(t,b))}if(QNn(p))return this.spawnPromise(p,t);var m=(i={id:t,send:function(b){return l.forEach(function(w){return w(b)})},subscribe:function(b){var w=r4e(b);return c.add(w.next),{unsubscribe:function(){c.delete(w.next)}}},stop:function(){o=!0,Zf(p)&&p()},toJSON:function(){return{id:t}},getSnapshot:function(){return d}},i[vG]=function(){return this},i);return this.children.set(t,m),m},n.prototype.spawnObservable=function(e,t){var i,r=this,o,l=e.subscribe(function(d){o=d,r.send(H2(d,{origin:t}))},function(d){r.removeChild(t),r.send(H2(uke(t,d),{origin:t}))},function(){r.removeChild(t),r.send(H2(bUe(t),{origin:t}))}),c=(i={id:t,send:function(){},subscribe:function(d,h,p){return e.subscribe(d,h,p)},stop:function(){return l.unsubscribe()},getSnapshot:function(){return o},toJSON:function(){return{id:t}}},i[vG]=function(){return this},i);return this.children.set(t,c),c},n.prototype.spawnActor=function(e,t){return this.children.set(t,e),e},n.prototype.spawnActivity=function(e){var t=this.machine.options&&this.machine.options.activities?this.machine.options.activities[e.type]:void 0;if(t){var i=t(this.state.context,e);this.spawnEffect(e.id,i)}},n.prototype.spawnEffect=function(e,t){var i;this.children.set(e,(i={id:e,send:function(){},subscribe:function(){return{unsubscribe:function(){}}},stop:t||void 0,getSnapshot:function(){},toJSON:function(){return{id:e}}},i[vG]=function(){return this},i))},n.prototype.attachDev=function(){var e=zzt();if(this.options.devTools&&e){if(e.__REDUX_DEVTOOLS_EXTENSION__){var t=typeof this.options.devTools=="object"?this.options.devTools:void 0;this.devTools=e.__REDUX_DEVTOOLS_EXTENSION__.connect(Ws(Ws({name:this.id,autoPause:!0,stateSanitizer:function(i){return{value:i.value,context:i.context,actions:i.actions}}},t),{features:Ws({jump:!1,skip:!1},t?t.features:void 0)}),this.machine),this.devTools.init(this.state)}Ffr(this)}},n.prototype.toJSON=function(){return{id:this.id}},n.prototype[vG]=function(){return this},n.prototype.getSnapshot=function(){return this.status===ib.NotStarted?this.initialState:this._state},n.defaultOptions={execute:!0,deferEvents:!0,clock:{setTimeout:function(e,t){return setTimeout(e,t)},clearTimeout:function(e){return clearTimeout(e)}},logger:console.log.bind(console),devTools:!1},n.interpret=Uci,n})(),Bfr=function(n){return qf(n)?Ws(Ws({},VOt),{name:n}):Ws(Ws(Ws({},VOt),{name:afr()}),n)};function hLe(n,e){var t=Bfr(e);return xfr(function(i){return i?i.spawn(n,t.name,t):$zt(n,t.name)})}function Uci(n,e){var t=new Hfr(n,e);return t}function Wfr(n){if(typeof n=="string"){var e={type:n};return e.toString=function(){return n},e}return n}function DBe(n){return Ws(Ws({type:POt},n),{toJSON:function(){n.onDone,n.onError;var e=jzt(n,["onDone","onError"]);return Ws(Ws({},e),{type:POt,src:Wfr(n.src)})}})}var IBe="",$Ot="#",AEt="*",ohe={},ahe=function(n){return n[0]===$Ot},Vfr=function(){return{actions:{},guards:{},services:{},activities:{},delays:{}}},$fr=(function(){function n(e,t,i,r){i===void 0&&(i="context"in e?e.context:void 0);var o=this,l;this.config=e,this._context=i,this.order=-1,this.__xstatenode=!0,this.__cache={events:void 0,relativeValue:new Map,initialStateValue:void 0,initialState:void 0,on:void 0,transitions:void 0,candidates:{},delayedTransitions:void 0},this.idMap={},this.tags=[],this.options=Object.assign(Vfr(),t),this.parent=r?.parent,this.key=this.config.key||r?.key||this.config.id||"(machine)",this.machine=this.parent?this.parent.machine:this,this.path=this.parent?this.parent.path.concat(this.key):[],this.delimiter=this.config.delimiter||(this.parent?this.parent.delimiter:Rci),this.id=this.config.id||$m([this.machine.key],xf(this.path),!1).join(this.delimiter),this.version=this.parent?this.parent.version:this.config.version,this.type=this.config.type||(this.config.parallel?"parallel":this.config.states&&Object.keys(this.config.states).length?"compound":this.config.history?"history":"atomic"),this.schema=this.parent?this.machine.schema:(l=this.config.schema)!==null&&l!==void 0?l:{},this.description=this.config.description,this.initial=this.config.initial,this.states=this.config.states?cke(this.config.states,function(h,p){var m,b=new n(h,{},void 0,{parent:o,key:p});return Object.assign(o.idMap,Ws((m={},m[b.id]=b,m),b.idMap)),b}):ohe;var c=0;function d(h){var p,m;h.order=c++;try{for(var b=yh(Wci(h)),w=b.next();!w.done;w=b.next()){var _=w.value;d(_)}}catch(x){p={error:x}}finally{try{w&&!w.done&&(m=b.return)&&m.call(b)}finally{if(p)throw p.error}}}d(this),this.history=this.config.history===!0?"shallow":this.config.history||!1,this._transient=!!this.config.always||(this.config.on?Array.isArray(this.config.on)?this.config.on.some(function(h){var p=h.event;return p===IBe}):IBe in this.config.on:!1),this.strict=!!this.config.strict,this.onEntry=AR(this.config.entry||this.config.onEntry).map(function(h){return uLe(h)}),this.onExit=AR(this.config.exit||this.config.onExit).map(function(h){return uLe(h)}),this.meta=this.config.meta,this.doneData=this.type==="final"?this.config.data:void 0,this.invoke=AR(this.config.invoke).map(function(h,p){var m,b;if(fme(h)){var w=TBe(o.id,p);return o.machine.options.services=Ws((m={},m[w]=h,m),o.machine.options.services),DBe({src:w,id:w})}else if(qf(h.src)){var w=h.id||TBe(o.id,p);return DBe(Ws(Ws({},h),{id:w,src:h.src}))}else if(fme(h.src)||Zf(h.src)){var w=h.id||TBe(o.id,p);return o.machine.options.services=Ws((b={},b[w]=h.src,b),o.machine.options.services),DBe(Ws(Ws({id:w},h),{src:w}))}else{var _=h.src;return DBe(Ws(Ws({id:TBe(o.id,p)},h),{src:_}))}}),this.activities=AR(this.config.activities).concat(this.invoke).map(function(h){return Vzt(h)}),this.transition=this.transition.bind(this),this.tags=AR(this.config.tags)}return n.prototype._init=function(){this.__cache.transitions||Vci(this).forEach(function(e){return e.on})},n.prototype.withConfig=function(e,t){var i=this.options,r=i.actions,o=i.activities,l=i.guards,c=i.services,d=i.delays;return new n(this.config,{actions:Ws(Ws({},r),e.actions),activities:Ws(Ws({},o),e.activities),guards:Ws(Ws({},l),e.guards),services:Ws(Ws({},c),e.services),delays:Ws(Ws({},d),e.delays)},t??this.context)},n.prototype.withContext=function(e){return new n(this.config,this.options,e)},Object.defineProperty(n.prototype,"context",{get:function(){return Zf(this._context)?this._context():this._context},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"definition",{get:function(){return{id:this.id,key:this.key,version:this.version,context:this.context,type:this.type,initial:this.initial,history:this.history,states:cke(this.states,function(e){return e.definition}),on:this.on,transitions:this.transitions,entry:this.onEntry,exit:this.onExit,activities:this.activities||[],meta:this.meta,order:this.order||-1,data:this.doneData,invoke:this.invoke,description:this.description,tags:this.tags}},enumerable:!1,configurable:!0}),n.prototype.toJSON=function(){return this.definition},Object.defineProperty(n.prototype,"on",{get:function(){if(this.__cache.on)return this.__cache.on;var e=this.transitions;return this.__cache.on=e.reduce(function(t,i){return t[i.eventType]=t[i.eventType]||[],t[i.eventType].push(i),t},{})},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"after",{get:function(){return this.__cache.delayedTransitions||(this.__cache.delayedTransitions=this.getDelayedTransitions(),this.__cache.delayedTransitions)},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"transitions",{get:function(){return this.__cache.transitions||(this.__cache.transitions=this.formatTransitions(),this.__cache.transitions)},enumerable:!1,configurable:!0}),n.prototype.getCandidates=function(e){if(this.__cache.candidates[e])return this.__cache.candidates[e];var t=e===IBe,i=this.transitions.filter(function(r){var o=r.eventType===e;return t?o:o||r.eventType===AEt});return this.__cache.candidates[e]=i,i},n.prototype.getDelayedTransitions=function(){var e=this,t=this.config.after;if(!t)return[];var i=function(o,l){var c=Zf(o)?"".concat(e.id,":delay[").concat(l,"]"):o,d=wfr(c,e.id);return e.onEntry.push(p5e(d,{delay:o})),e.onExit.push(pfr(d)),d},r=K1e(t)?t.map(function(o,l){var c=i(o.delay,l);return Ws(Ws({},o),{event:c})}):h0(Object.keys(t).map(function(o,l){var c=t[o],d=qf(c)?{target:c}:c,h=isNaN(+o)?o:+o,p=i(h,l);return AR(d).map(function(m){return Ws(Ws({},m),{event:p,delay:h})})}));return r.map(function(o){var l=o.delay;return Ws(Ws({},e.formatTransition(o)),{delay:l})})},n.prototype.getStateNodes=function(e){var t,i=this;if(!e)return[];var r=e instanceof f6?e.value:i4e(e,this.delimiter);if(qf(r)){var o=this.getStateNode(r).initial;return o!==void 0?this.getStateNodes((t={},t[r]=o,t)):[this,this.states[r]]}var l=Object.keys(r),c=[this];return c.push.apply(c,$m([],xf(h0(l.map(function(d){return i.getStateNode(d).getStateNodes(r[d])}))),!1)),c},n.prototype.handles=function(e){var t=Mci(e);return this.events.includes(t)},n.prototype.resolveState=function(e){var t=e instanceof f6?e:f6.create(e),i=Array.from(dke([],this.getStateNodes(t.value)));return new f6(Ws(Ws({},t),{value:this.resolve(t.value),configuration:i,done:vUe(i,this),tags:ePn(i),machine:this.machine}))},n.prototype.transitionLeafNode=function(e,t,i){var r=this.getStateNode(e),o=r.next(t,i);return!o||!o.transitions.length?this.next(t,i):o},n.prototype.transitionCompoundNode=function(e,t,i){var r=Object.keys(e),o=this.getStateNode(r[0]),l=o._transition(e[r[0]],t,i);return!l||!l.transitions.length?this.next(t,i):l},n.prototype.transitionParallelNode=function(e,t,i){var r,o,l={};try{for(var c=yh(Object.keys(e)),d=c.next();!d.done;d=c.next()){var h=d.value,p=e[h];if(p){var m=this.getStateNode(h),b=m._transition(p,t,i);b&&(l[h]=b)}}}catch(I){r={error:I}}finally{try{d&&!d.done&&(o=c.return)&&o.call(c)}finally{if(r)throw r.error}}var w=Object.keys(l).map(function(I){return l[I]}),_=h0(w.map(function(I){return I.transitions})),x=w.some(function(I){return I.transitions.length>0});if(!x)return this.next(t,i);var T=h0(Object.keys(l).map(function(I){return l[I].configuration}));return{transitions:_,exitSet:h0(w.map(function(I){return I.exitSet})),configuration:T,source:t,actions:h0(Object.keys(l).map(function(I){return l[I].actions}))}},n.prototype._transition=function(e,t,i){return qf(e)?this.transitionLeafNode(e,t,i):Object.keys(e).length===1?this.transitionCompoundNode(e,t,i):this.transitionParallelNode(e,t,i)},n.prototype.getTransitionData=function(e,t){return this._transition(e.value,e,H2(t))},n.prototype.next=function(e,t){var i,r,o=this,l=t.name,c=[],d=[],h;try{for(var p=yh(this.getCandidates(l)),m=p.next();!m.done;m=p.next()){var b=m.value,w=b.cond,_=b.in,x=e.context,T=_?qf(_)&&ahe(_)?e.matches(i4e(this.getStateNodeById(_).path,this.delimiter)):Wzt(i4e(_,this.delimiter),Qhr(this.path.slice(0,-2))(e.value)):!0,I=!1;try{I=!w||Fci(this.machine,w,x,t,e)}catch(M){throw new Error("Unable to evaluate guard '".concat(w.name||w.type,"' in transition for event '").concat(l,"' in state node '").concat(this.id,`': -`).concat(M.message))}if(I&&T){b.target!==void 0&&(d=b.target),c.push.apply(c,$m([],xf(b.actions),!1)),h=b;break}}}catch(M){i={error:M}}finally{try{m&&!m.done&&(r=p.return)&&r.call(p)}finally{if(i)throw i.error}}if(h){if(!d.length)return{transitions:[h],exitSet:[],configuration:e.value?[this]:[],source:e,actions:c};var L=h0(d.map(function(M){return o.getRelativeStateNodes(M,e.historyValue)})),A=!!h.internal;return{transitions:[h],exitSet:A?[]:h0(d.map(function(M){return o.getPotentiallyReenteringNodes(M)})),configuration:L,source:e,actions:c}}},n.prototype.getPotentiallyReenteringNodes=function(e){if(this.order0,w=b?e.configuration:t?t.configuration:[],_=vUe(w,this),x=b?Dfr(this.machine,m):void 0,T=t?t.historyValue?t.historyValue:e.source?this.machine.historyValue(t.value):void 0:void 0,I=this.getActions(new Set(w),_,e,i,o,t,r),L=t?Ws({},t.activities):{};try{for(var A=yh(I),M=A.next();!M.done;M=A.next()){var O=M.value;try{for(var F=(d=void 0,yh(O.actions)),j=F.next();!j.done;j=F.next()){var W=j.value;W.type===NOt?L[W.activity.id||W.activity.type]=W:W.type===Hzt&&(L[W.activity.id||W.activity.type]=!1)}}catch(pe){d={error:pe}}finally{try{j&&!j.done&&(h=F.return)&&h.call(F)}finally{if(d)throw d.error}}}}catch(pe){l={error:pe}}finally{try{M&&!M.done&&(c=A.return)&&c.call(A)}finally{if(l)throw l.error}}var q=xf(Qqe(this,t,i,o,I,r,this.machine.config.predictableActionArguments||this.machine.config.preserveActionOrder),2),Z=q[0],ee=q[1],G=xf(nfr(Z,HOt),2),te=G[0],Q=G[1],ie=Z.filter(function(pe){var me;return pe.type===NOt&&((me=pe.activity)===null||me===void 0?void 0:me.type)===POt}),se=ie.reduce(function(pe,me){return pe[me.activity.id]=Efr(me.activity,p.machine,ee,o),pe},t?Ws({},t.children):{}),de=new f6({value:x||t.value,context:ee,_event:o,_sessionid:t?t._sessionid:null,historyValue:x?T?ifr(T,x):void 0:t?t.historyValue:void 0,history:!x||e.source?t:void 0,actions:x?Q:[],activities:x?L:t?t.activities:{},events:[],configuration:w,transitions:e.transitions,children:se,done:_,tags:ePn(w),machine:this}),ne=i!==ee;de.changed=o.name===Aci||ne;var we=de.history;we&&delete we.history;var ue=!_&&(this._transient||m.some(function(pe){return pe._transient}));if(!b&&(!ue||o.name===IBe))return de;var ce=de;if(!_)for(ue&&(ce=this.resolveRaisedTransition(ce,{type:Uhr},o,r));te.length;){var ye=te.shift();ce=this.resolveRaisedTransition(ce,ye._event,o,r)}var he=ce.changed||(we?!!ce.actions.length||ne||typeof we.value!=typeof ce.value||!zci(ce.value,we.value):void 0);return ce.changed=he,ce.history=we,ce},n.prototype.getStateNode=function(e){if(ahe(e))return this.machine.getStateNodeById(e);if(!this.states)throw new Error("Unable to retrieve child state '".concat(e,"' from '").concat(this.id,"'; no child states exist."));var t=this.states[e];if(!t)throw new Error("Child state '".concat(e,"' does not exist on '").concat(this.id,"'"));return t},n.prototype.getStateNodeById=function(e){var t=ahe(e)?e.slice($Ot.length):e;if(t===this.id)return this;var i=this.machine.idMap[t];if(!i)throw new Error("Child state node '#".concat(t,"' does not exist on machine '").concat(this.id,"'"));return i},n.prototype.getStateNodeByPath=function(e){if(typeof e=="string"&&ahe(e))try{return this.getStateNodeById(e.slice(1))}catch{}for(var t=jOt(e,this.delimiter).slice(),i=this;t.length;){var r=t.shift();if(!r.length)break;i=i.getStateNode(r)}return i},n.prototype.resolve=function(e){var t,i=this;if(!e)return this.initialStateValue||ohe;switch(this.type){case"parallel":return cke(this.initialStateValue,function(o,l){return o?i.getStateNode(l).resolve(e[l]||o):ohe});case"compound":if(qf(e)){var r=this.getStateNode(e);return r.type==="parallel"||r.type==="compound"?(t={},t[e]=r.initialStateValue,t):e}return Object.keys(e).length?cke(e,function(o,l){return o?i.getStateNode(l).resolve(o):ohe}):this.initialStateValue||{};default:return e||ohe}},n.prototype.getResolvedPath=function(e){if(ahe(e)){var t=this.machine.idMap[e.slice($Ot.length)];if(!t)throw new Error("Unable to find state node '".concat(e,"'"));return t.path}return jOt(e,this.delimiter)},Object.defineProperty(n.prototype,"initialStateValue",{get:function(){var e;if(this.__cache.initialStateValue)return this.__cache.initialStateValue;var t;if(this.type==="parallel")t=XNn(this.states,function(i){return i.initialStateValue||ohe},function(i){return i.type!=="history"});else if(this.initial!==void 0){if(!this.states[this.initial])throw new Error("Initial state '".concat(this.initial,"' not found on '").concat(this.key,"'"));t=eGe(this.states[this.initial])?this.initial:(e={},e[this.initial]=this.states[this.initial].initialStateValue,e)}else t={};return this.__cache.initialStateValue=t,this.__cache.initialStateValue},enumerable:!1,configurable:!0}),n.prototype.getInitialState=function(e,t){this._init();var i=this.getStateNodes(e);return this.resolveTransition({configuration:i,exitSet:[],transitions:[],source:void 0,actions:[]},void 0,t??this.machine.context,void 0)},Object.defineProperty(n.prototype,"initialState",{get:function(){var e=this.initialStateValue;if(!e)throw new Error("Cannot retrieve initial state from simple state '".concat(this.id,"'."));return this.getInitialState(e)},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"target",{get:function(){var e;if(this.type==="history"){var t=this.config;qf(t.target)?e=ahe(t.target)?Zqe(this.machine.getStateNodeById(t.target).path.slice(this.path.length-1)):t.target:e=t.target}return e},enumerable:!1,configurable:!0}),n.prototype.getRelativeStateNodes=function(e,t,i){return i===void 0&&(i=!0),i?e.type==="history"?e.resolveHistory(t):e.initialStateNodes:[e]},Object.defineProperty(n.prototype,"initialStateNodes",{get:function(){var e=this;if(eGe(this))return[this];if(this.type==="compound"&&!this.initial)return[this];var t=mUe(this.initialStateValue);return h0(t.map(function(i){return e.getFromRelativePath(i)}))},enumerable:!1,configurable:!0}),n.prototype.getFromRelativePath=function(e){if(!e.length)return[this];var t=xf(e),i=t[0],r=t.slice(1);if(!this.states)throw new Error("Cannot retrieve subPath '".concat(i,"' from node with no states"));var o=this.getStateNode(i);if(o.type==="history")return o.resolveHistory();if(!this.states[i])throw new Error("Child state '".concat(i,"' does not exist on '").concat(this.id,"'"));return this.states[i].getFromRelativePath(r)},n.prototype.historyValue=function(e){if(Object.keys(this.states).length)return{current:e||this.initialStateValue,states:XNn(this.states,function(t,i){if(!e)return t.historyValue();var r=qf(e)?void 0:e[i];return t.historyValue(r||t.initialStateValue)},function(t){return!t.history})}},n.prototype.resolveHistory=function(e){var t=this;if(this.type!=="history")return[this];var i=this.parent;if(!e){var r=this.target;return r?h0(mUe(r).map(function(l){return i.getFromRelativePath(l)})):i.initialStateNodes}var o=Jhr(i.path,"states")(e).current;return qf(o)?[i.getStateNode(o)]:h0(mUe(o).map(function(l){return t.history==="deep"?i.getFromRelativePath(l):[i.states[l[0]]]}))},Object.defineProperty(n.prototype,"stateIds",{get:function(){var e=this,t=h0(Object.keys(this.states).map(function(i){return e.states[i].stateIds}));return[this.id].concat(t)},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"events",{get:function(){var e,t,i,r;if(this.__cache.events)return this.__cache.events;var o=this.states,l=new Set(this.ownEvents);if(o)try{for(var c=yh(Object.keys(o)),d=c.next();!d.done;d=c.next()){var h=d.value,p=o[h];if(p.states)try{for(var m=(i=void 0,yh(p.events)),b=m.next();!b.done;b=m.next()){var w=b.value;l.add("".concat(w))}}catch(_){i={error:_}}finally{try{b&&!b.done&&(r=m.return)&&r.call(m)}finally{if(i)throw i.error}}}}catch(_){e={error:_}}finally{try{d&&!d.done&&(t=c.return)&&t.call(c)}finally{if(e)throw e.error}}return this.__cache.events=Array.from(l)},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"ownEvents",{get:function(){var e=new Set(this.transitions.filter(function(t){return!(!t.target&&!t.actions.length&&t.internal)}).map(function(t){return t.eventType}));return Array.from(e)},enumerable:!1,configurable:!0}),n.prototype.resolveTarget=function(e){var t=this;if(e!==void 0)return e.map(function(i){if(!qf(i))return i;var r=i[0]===t.delimiter;if(r&&!t.parent)return t.getStateNodeByPath(i.slice(1));var o=r?t.key+i:i;if(t.parent)try{var l=t.parent.getStateNodeByPath(o);return l}catch(c){throw new Error("Invalid transition definition for state node '".concat(t.id,`': -`).concat(c.message))}else return t.getStateNodeByPath(o)})},n.prototype.formatTransition=function(e){var t=this,i=lfr(e.target),r="internal"in e?e.internal:i?i.some(function(d){return qf(d)&&d[0]===t.delimiter}):!0,o=this.machine.options.guards,l=this.resolveTarget(i),c=Ws(Ws({},e),{actions:wG(AR(e.actions)),cond:Pci(e.cond,o),target:l,source:this,internal:r,eventType:e.event,toJSON:function(){return Ws(Ws({},c),{target:c.target?c.target.map(function(d){return"#".concat(d.id)}):void 0,source:"#".concat(t.id)})}});return c},n.prototype.formatTransitions=function(){var e,t,i=this,r;if(!this.config.on)r=[];else if(Array.isArray(this.config.on))r=this.config.on;else{var o=this.config.on,l=AEt,c=o[l],d=c===void 0?[]:c,h=jzt(o,[typeof l=="symbol"?l:l+""]);r=h0(Object.keys(h).map(function(L){var A=she(L,h[L]);return A}).concat(she(AEt,d)))}var p=this.config.always?she("",this.config.always):[],m=this.config.onDone?she(String(LBe(this.id)),this.config.onDone):[],b=h0(this.invoke.map(function(L){var A=[];return L.onDone&&A.push.apply(A,$m([],xf(she(String(bUe(L.id)),L.onDone)),!1)),L.onError&&A.push.apply(A,$m([],xf(she(String(uke(L.id)),L.onError)),!1)),A})),w=this.after,_=h0($m($m($m($m([],xf(m),!1),xf(b),!1),xf(r),!1),xf(p),!1).map(function(L){return AR(L).map(function(A){return i.formatTransition(A)})}));try{for(var x=yh(w),T=x.next();!T.done;T=x.next()){var I=T.value;_.push(I)}}catch(L){e={error:L}}finally{try{T&&!T.done&&(t=x.return)&&t.call(x)}finally{if(e)throw e.error}}return _},n})();function Qg(n,e){return new $fr(n,e)}var yn=vfr,OY=p5e,Jpe=hfr,ep=dfr,V3=_fr,NY=Hci,qci=yfr,zfr=Cfr;const c7=D.createContext({setMessage:()=>null});function of({props:n,name:e}){return Azt({props:n,name:e,defaultTheme:Fet,themeId:w3})}function Ufr({theme:n,...e}){const t=w3 in n?n[w3]:void 0;return k.jsx(vci,{...e,themeId:t?w3:void 0,theme:t||n})}const ABe={colorSchemeStorageKey:"mui-color-scheme",defaultLightColorScheme:"light",defaultDarkColorScheme:"dark",modeStorageKey:"mui-mode"},{CssVarsProvider:qfr}=Mdr({themeId:w3,theme:()=>G1e({cssVariables:!0}),colorSchemeStorageKey:ABe.colorSchemeStorageKey,modeStorageKey:ABe.modeStorageKey,defaultColorScheme:{light:ABe.defaultLightColorScheme,dark:ABe.defaultDarkColorScheme},resolveTheme:n=>{const e={...n,typography:Sci(n.palette,n.typography)};return e.unstable_sx=function(i){return ZK({sx:i,theme:this})},e}}),Gfr=qfr;function Gci({theme:n,...e}){const t=D.useMemo(()=>{if(typeof n=="function")return n;const i=w3 in n?n[w3]:n;return"colorSchemes"in i?null:"vars"in i?n:{...n,vars:null}},[n]);return t?k.jsx(Ufr,{theme:t,...e}):k.jsx(Gfr,{theme:n,...e})}function zOt(...n){return n.reduce((e,t)=>t==null?e:function(...r){e.apply(this,r),t.apply(this,r)},()=>{})}const Gs=Edr;function Kfr(n){return No("MuiSvgIcon",n)}Po("MuiSvgIcon",["root","colorPrimary","colorSecondary","colorAction","colorError","colorDisabled","fontSizeInherit","fontSizeSmall","fontSizeMedium","fontSizeLarge"]);const Yfr=n=>{const{color:e,fontSize:t,classes:i}=n,r={root:["root",e!=="inherit"&&`color${ii(e)}`,`fontSize${ii(t)}`]};return Fo(r,Kfr,i)},Zfr=tn("svg",{name:"MuiSvgIcon",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.color!=="inherit"&&e[`color${ii(t.color)}`],e[`fontSize${ii(t.fontSize)}`]]}})(Gs(({theme:n})=>({userSelect:"none",width:"1em",height:"1em",display:"inline-block",flexShrink:0,transition:n.transitions?.create?.("fill",{duration:(n.vars??n).transitions?.duration?.shorter}),variants:[{props:e=>!e.hasSvgAsChild,style:{fill:"currentColor"}},{props:{fontSize:"inherit"},style:{fontSize:"inherit"}},{props:{fontSize:"small"},style:{fontSize:n.typography?.pxToRem?.(20)||"1.25rem"}},{props:{fontSize:"medium"},style:{fontSize:n.typography?.pxToRem?.(24)||"1.5rem"}},{props:{fontSize:"large"},style:{fontSize:n.typography?.pxToRem?.(35)||"2.1875rem"}},...Object.entries((n.vars??n).palette).filter(([,e])=>e&&e.main).map(([e])=>({props:{color:e},style:{color:(n.vars??n).palette?.[e]?.main}})),{props:{color:"action"},style:{color:(n.vars??n).palette?.action?.active}},{props:{color:"disabled"},style:{color:(n.vars??n).palette?.action?.disabled}},{props:{color:"inherit"},style:{color:void 0}}]}))),tGe=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiSvgIcon"}),{children:r,className:o,color:l="inherit",component:c="svg",fontSize:d="medium",htmlColor:h,inheritViewBox:p=!1,titleAccess:m,viewBox:b="0 0 24 24",...w}=i,_=D.isValidElement(r)&&r.type==="svg",x={...i,color:l,component:c,fontSize:d,instanceFontSize:e.fontSize,inheritViewBox:p,viewBox:b,hasSvgAsChild:_},T={};p||(T.viewBox=b);const I=Yfr(x);return k.jsxs(Zfr,{as:c,className:_i(I.root,o),focusable:"false",color:h,"aria-hidden":m?void 0:!0,role:m?"img":void 0,ref:t,...T,...w,..._&&r.props,ownerState:x,children:[_?r.props.children:r,m?k.jsx("title",{children:m}):null]})});tGe.muiName="SvgIcon";function Ya(n,e){function t(i,r){return k.jsx(tGe,{"data-testid":void 0,ref:r,...i,children:n})}return t.muiName=tGe.muiName,D.memo(D.forwardRef(t))}function g5e(n,e=166){let t;function i(...r){const o=()=>{n.apply(this,r)};clearTimeout(t),t=setTimeout(o,e)}return i.clear=()=>{clearTimeout(t)},i}function hv(n){return n&&n.ownerDocument||document}function Y6(n){return hv(n).defaultView||window}function UOt(n,e){typeof n=="function"?n(e):n&&(n.current=e)}function S9(n){const{controlled:e,default:t,name:i,state:r="value"}=n,{current:o}=D.useRef(e!==void 0),[l,c]=D.useState(t),d=o?e:l,h=D.useCallback(p=>{o||c(p)},[]);return[d,h]}function ub(n){const e=D.useRef(n);return IS(()=>{e.current=n}),D.useRef((...t)=>(0,e.current)(...t)).current}function xm(...n){const e=D.useRef(void 0),t=D.useCallback(i=>{const r=n.map(o=>{if(o==null)return null;if(typeof o=="function"){const l=o,c=l(i);return typeof c=="function"?c:()=>{l(null)}}return o.current=i,()=>{o.current=null}});return()=>{r.forEach(o=>o?.())}},n);return D.useMemo(()=>n.every(i=>i==null)?null:i=>{e.current&&(e.current(),e.current=void 0),i!=null&&(e.current=t(i))},n)}function Xfr(n,e){const t=n.charCodeAt(2);return n[0]==="o"&&n[1]==="n"&&t>=65&&t<=90&&typeof e=="function"}function Uzt(n,e){if(!n)return e;function t(l,c){const d={};return Object.keys(c).forEach(h=>{Xfr(h,c[h])&&typeof l[h]=="function"&&(d[h]=(...p)=>{l[h](...p),c[h](...p)})}),d}if(typeof n=="function"||typeof e=="function")return l=>{const c=typeof e=="function"?e(l):e,d=typeof n=="function"?n({...l,...c}):n,h=_i(l?.className,c?.className,d?.className),p=t(d,c);return{...c,...d,...p,...!!h&&{className:h},...c?.style&&d?.style&&{style:{...c.style,...d.style}},...c?.sx&&d?.sx&&{sx:[...Array.isArray(c.sx)?c.sx:[c.sx],...Array.isArray(d.sx)?d.sx:[d.sx]]}}};const i=e,r=t(n,i),o=_i(i?.className,n?.className);return{...e,...n,...r,...!!o&&{className:o},...i?.style&&n?.style&&{style:{...i.style,...n.style}},...i?.sx&&n?.sx&&{sx:[...Array.isArray(i.sx)?i.sx:[i.sx],...Array.isArray(n.sx)?n.sx:[n.sx]]}}}function uc(n,e){if(n==null)return{};var t={};for(var i in n)if({}.hasOwnProperty.call(n,i)){if(e.indexOf(i)!==-1)continue;t[i]=n[i]}return t}function nGe(n,e){return nGe=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,i){return t.__proto__=i,t},nGe(n,e)}function ZW(n,e){n.prototype=Object.create(e.prototype),n.prototype.constructor=n,nGe(n,e)}function Qfr(n,e){return n.classList?!!e&&n.classList.contains(e):(" "+(n.className.baseVal||n.className)+" ").indexOf(" "+e+" ")!==-1}function Jfr(n,e){n.classList?n.classList.add(e):Qfr(n,e)||(typeof n.className=="string"?n.className=n.className+" "+e:n.setAttribute("class",(n.className&&n.className.baseVal||"")+" "+e))}function nPn(n,e){return n.replace(new RegExp("(^|\\s)"+e+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function epr(n,e){n.classList?n.classList.remove(e):typeof n.className=="string"?n.className=nPn(n.className,e):n.setAttribute("class",nPn(n.className&&n.className.baseVal||"",e))}var REt={exports:{}},y4={},MEt={exports:{}},OEt={};var iPn;function tpr(){return iPn||(iPn=1,(function(n){function e(ne,we){var ue=ne.length;ne.push(we);e:for(;0>>1,ye=ne[ce];if(0>>1;cer(me,ue))ber(xe,me)?(ne[ce]=xe,ne[be]=ue,ce=be):(ne[ce]=me,ne[pe]=ue,ce=pe);else if(ber(xe,ue))ne[ce]=xe,ne[be]=ue,ce=be;else break e}}return we}function r(ne,we){var ue=ne.sortIndex-we.sortIndex;return ue!==0?ue:ne.id-we.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;n.unstable_now=function(){return o.now()}}else{var l=Date,c=l.now();n.unstable_now=function(){return l.now()-c}}var d=[],h=[],p=1,m=null,b=3,w=!1,_=!1,x=!1,T=typeof setTimeout=="function"?setTimeout:null,I=typeof clearTimeout=="function"?clearTimeout:null,L=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function A(ne){for(var we=t(h);we!==null;){if(we.callback===null)i(h);else if(we.startTime<=ne)i(h),we.sortIndex=we.expirationTime,e(d,we);else break;we=t(h)}}function M(ne){if(x=!1,A(ne),!_)if(t(d)!==null)_=!0,se(O);else{var we=t(h);we!==null&&de(M,we.startTime-ne)}}function O(ne,we){_=!1,x&&(x=!1,I(W),W=-1),w=!0;var ue=b;try{for(A(we),m=t(d);m!==null&&(!(m.expirationTime>we)||ne&&!ee());){var ce=m.callback;if(typeof ce=="function"){m.callback=null,b=m.priorityLevel;var ye=ce(m.expirationTime<=we);we=n.unstable_now(),typeof ye=="function"?m.callback=ye:m===t(d)&&i(d),A(we)}else i(d);m=t(d)}if(m!==null)var he=!0;else{var pe=t(h);pe!==null&&de(M,pe.startTime-we),he=!1}return he}finally{m=null,b=ue,w=!1}}var F=!1,j=null,W=-1,q=5,Z=-1;function ee(){return!(n.unstable_now()-Zne||125ce?(ne.sortIndex=ue,e(h,ne),t(d)===null&&ne===t(h)&&(x?(I(W),W=-1):x=!0,de(M,ue-ce))):(ne.sortIndex=ye,e(d,ne),_||w||(_=!0,se(O))),ne},n.unstable_shouldYield=ee,n.unstable_wrapCallback=function(ne){var we=b;return function(){var ue=b;b=we;try{return ne.apply(this,arguments)}finally{b=ue}}}})(OEt)),OEt}var rPn;function npr(){return rPn||(rPn=1,MEt.exports=tpr()),MEt.exports}var sPn;function ipr(){if(sPn)return y4;sPn=1;var n=LM(),e=npr();function t(P){for(var B="https://reactjs.org/docs/error-decoder.html?invariant="+P,J=1;J"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),d=Object.prototype.hasOwnProperty,h=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,p={},m={};function b(P){return d.call(m,P)?!0:d.call(p,P)?!1:h.test(P)?m[P]=!0:(p[P]=!0,!1)}function w(P,B,J,le){if(J!==null&&J.type===0)return!1;switch(typeof B){case"function":case"symbol":return!0;case"boolean":return le?!1:J!==null?!J.acceptsBooleans:(P=P.toLowerCase().slice(0,5),P!=="data-"&&P!=="aria-");default:return!1}}function _(P,B,J,le){if(B===null||typeof B>"u"||w(P,B,J,le))return!0;if(le)return!1;if(J!==null)switch(J.type){case 3:return!B;case 4:return B===!1;case 5:return isNaN(B);case 6:return isNaN(B)||1>B}return!1}function x(P,B,J,le,ke,je,gt){this.acceptsBooleans=B===2||B===3||B===4,this.attributeName=le,this.attributeNamespace=ke,this.mustUseProperty=J,this.propertyName=P,this.type=B,this.sanitizeURL=je,this.removeEmptyString=gt}var T={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(P){T[P]=new x(P,0,!1,P,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(P){var B=P[0];T[B]=new x(B,1,!1,P[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(P){T[P]=new x(P,2,!1,P.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(P){T[P]=new x(P,2,!1,P,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(P){T[P]=new x(P,3,!1,P.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(P){T[P]=new x(P,3,!0,P,null,!1,!1)}),["capture","download"].forEach(function(P){T[P]=new x(P,4,!1,P,null,!1,!1)}),["cols","rows","size","span"].forEach(function(P){T[P]=new x(P,6,!1,P,null,!1,!1)}),["rowSpan","start"].forEach(function(P){T[P]=new x(P,5,!1,P.toLowerCase(),null,!1,!1)});var I=/[\-:]([a-z])/g;function L(P){return P[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(P){var B=P.replace(I,L);T[B]=new x(B,1,!1,P,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(P){var B=P.replace(I,L);T[B]=new x(B,1,!1,P,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(P){var B=P.replace(I,L);T[B]=new x(B,1,!1,P,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(P){T[P]=new x(P,1,!1,P.toLowerCase(),null,!1,!1)}),T.xlinkHref=new x("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(P){T[P]=new x(P,1,!1,P.toLowerCase(),null,!0,!0)});function A(P,B,J,le){var ke=T.hasOwnProperty(B)?T[B]:null;(ke!==null?ke.type!==0:le||!(2{if(!Number.isNaN(+n))return+n;const e=n.match(/\d*\.?\d+/g);if(!e)return 0;let t=0;for(let i=0;iF_(_,x),w),w.unstable_sxConfig={...a5e,...p?.unstable_sxConfig},w.unstable_sx=function(x){return XK({sx:x,theme:this})},w.toRuntimeSource=Lci,Ohr(w),w}function ROt(n){let e;return n<1?e=5.11916*n**2:e=4.5*Math.log(n+1)+2,Math.round(e*10)/1e3}const Nhr=[...Array(25)].map((n,e)=>{if(e===0)return"none";const t=ROt(e);return`linear-gradient(rgba(255 255 255 / ${t}), rgba(255 255 255 / ${t}))`});function Dci(n){return{inputPlaceholder:n==="dark"?.5:.42,inputUnderline:n==="dark"?.7:.42,switchTrackDisabled:n==="dark"?.2:.12,switchTrack:n==="dark"?.3:.38}}function Ici(n){return n==="dark"?Nhr:[]}function Phr(n){const{palette:e={mode:"light"},opacity:t,overlays:i,colorSpace:r,...o}=n,l=Ozt({...e,colorSpace:r});return{palette:l,opacity:{...Dci(l.mode),...t},overlays:i||Ici(l.mode),...o}}function Fhr(n){return!!n[0].match(/(cssVarPrefix|colorSchemeSelector|modularCssLayers|rootSelector|typography|mixins|breakpoints|direction|transitions)/)||!!n[0].match(/sxConfig$/)||n[0]==="palette"&&!!n[1]?.match(/(mode|contrastThreshold|tonalOffset)/)}const jhr=n=>[...[...Array(25)].map((e,t)=>`--${n?`${n}-`:""}overlays-${t}`),`--${n?`${n}-`:""}palette-AppBar-darkBg`,`--${n?`${n}-`:""}palette-AppBar-darkColor`],Hhr=n=>(e,t)=>{const i=n.rootSelector||":root",r=n.colorSchemeSelector;let o=r;if(r==="class"&&(o=".%s"),r==="data"&&(o="[data-%s]"),r?.startsWith("data-")&&!r.includes("%s")&&(o=`[${r}="%s"]`),n.defaultColorScheme===e){if(e==="dark"){const l={};return jhr(n.cssVarPrefix).forEach(c=>{l[c]=t[c],delete t[c]}),o==="media"?{[i]:t,"@media (prefers-color-scheme: dark)":{[i]:l}}:o?{[o.replace("%s",e)]:l,[`${i}, ${o.replace("%s",e)}`]:t}:{[i]:{...t,...l}}}if(o&&o!=="media")return`${i}, ${o.replace("%s",String(e))}`}else if(e){if(o==="media")return{[`@media (prefers-color-scheme: ${String(e)})`]:{[i]:t}};if(o)return o.replace("%s",String(e))}return i};function Bhr(n,e){e.forEach(t=>{n[t]||(n[t]={})})}function er(n,e,t){!n[e]&&t&&(n[e]=t)}function oke(n){return typeof n!="string"||!n.startsWith("hsl")?n:bci(n)}function xH(n,e){`${e}Channel`in n||(n[`${e}Channel`]=ske(oke(n[e])))}function Whr(n){return typeof n=="number"?`${n}px`:typeof n=="string"||typeof n=="function"||Array.isArray(n)?n:"8px"}const cP=n=>{try{return n()}catch{}},Vhr=(n="mui")=>Bdr(n);function DEt(n,e,t,i,r){if(!t)return;t=t===!0?{}:t;const o=r==="dark"?"dark":"light";if(!i){e[r]=Phr({...t,palette:{mode:o,...t?.palette},colorSpace:n});return}const{palette:l,...c}=AOt({...i,palette:{mode:o,...t?.palette},colorSpace:n});return e[r]={...t,palette:l,opacity:{...Dci(o),...t?.opacity},overlays:t?.overlays||Ici(o)},c}function $hr(n={},...e){const{colorSchemes:t={light:!0},defaultColorScheme:i,disableCssColorScheme:r=!1,cssVarPrefix:o="mui",nativeColor:l=!1,shouldSkipGeneratingVar:c=Fhr,colorSchemeSelector:d=t.light&&t.dark?"media":void 0,rootSelector:h=":root",...p}=n,m=Object.keys(t)[0],b=i||(t.light&&m!=="light"?"light":m),w=Vhr(o),{[b]:_,light:x,dark:T,...I}=t,D={...I};let A=_;if((b==="dark"&&!("dark"in t)||b==="light"&&!("light"in t))&&(A=!0),!A)throw new Error(mW(21,b));let M;l&&(M="oklch");const O=DEt(M,D,A,p,b);x&&!D.light&&DEt(M,D,x,void 0,"light"),T&&!D.dark&&DEt(M,D,T,void 0,"dark");let F={defaultColorScheme:b,...O,cssVarPrefix:o,colorSchemeSelector:d,rootSelector:h,getCssVar:w,colorSchemes:D,font:{..._hr(O.typography),...O.font},spacing:Whr(p.spacing)};Object.keys(F.colorSchemes).forEach(te=>{const G=F.colorSchemes[te].palette,ee=ie=>{const se=ie.split("-"),ue=se[1],ne=se[2];return w(ie,G[ue][ne])};G.mode==="light"&&(er(G.common,"background","#fff"),er(G.common,"onBackground","#000")),G.mode==="dark"&&(er(G.common,"background","#000"),er(G.common,"onBackground","#fff"));function Q(ie,se,ue){if(M){let ne;return ie===dte&&(ne=`transparent ${((1-ue)*100).toFixed(0)}%`),ie===Up&&(ne=`#000 ${(ue*100).toFixed(0)}%`),ie===qp&&(ne=`#fff ${(ue*100).toFixed(0)}%`),`color-mix(in ${M}, ${se}, ${ne})`}return ie(se,ue)}if(Bhr(G,["Alert","AppBar","Avatar","Button","Chip","FilledInput","LinearProgress","Skeleton","Slider","SnackbarContent","SpeedDialAction","StepConnector","StepContent","Switch","TableCell","Tooltip"]),G.mode==="light"){er(G.Alert,"errorColor",Q(Up,l?w("palette-error-light"):G.error.light,.6)),er(G.Alert,"infoColor",Q(Up,l?w("palette-info-light"):G.info.light,.6)),er(G.Alert,"successColor",Q(Up,l?w("palette-success-light"):G.success.light,.6)),er(G.Alert,"warningColor",Q(Up,l?w("palette-warning-light"):G.warning.light,.6)),er(G.Alert,"errorFilledBg",ee("palette-error-main")),er(G.Alert,"infoFilledBg",ee("palette-info-main")),er(G.Alert,"successFilledBg",ee("palette-success-main")),er(G.Alert,"warningFilledBg",ee("palette-warning-main")),er(G.Alert,"errorFilledColor",cP(()=>G.getContrastText(G.error.main))),er(G.Alert,"infoFilledColor",cP(()=>G.getContrastText(G.info.main))),er(G.Alert,"successFilledColor",cP(()=>G.getContrastText(G.success.main))),er(G.Alert,"warningFilledColor",cP(()=>G.getContrastText(G.warning.main))),er(G.Alert,"errorStandardBg",Q(qp,l?w("palette-error-light"):G.error.light,.9)),er(G.Alert,"infoStandardBg",Q(qp,l?w("palette-info-light"):G.info.light,.9)),er(G.Alert,"successStandardBg",Q(qp,l?w("palette-success-light"):G.success.light,.9)),er(G.Alert,"warningStandardBg",Q(qp,l?w("palette-warning-light"):G.warning.light,.9)),er(G.Alert,"errorIconColor",ee("palette-error-main")),er(G.Alert,"infoIconColor",ee("palette-info-main")),er(G.Alert,"successIconColor",ee("palette-success-main")),er(G.Alert,"warningIconColor",ee("palette-warning-main")),er(G.AppBar,"defaultBg",ee("palette-grey-100")),er(G.Avatar,"defaultBg",ee("palette-grey-400")),er(G.Button,"inheritContainedBg",ee("palette-grey-300")),er(G.Button,"inheritContainedHoverBg",ee("palette-grey-A100")),er(G.Chip,"defaultBorder",ee("palette-grey-400")),er(G.Chip,"defaultAvatarColor",ee("palette-grey-700")),er(G.Chip,"defaultIconColor",ee("palette-grey-700")),er(G.FilledInput,"bg","rgba(0, 0, 0, 0.06)"),er(G.FilledInput,"hoverBg","rgba(0, 0, 0, 0.09)"),er(G.FilledInput,"disabledBg","rgba(0, 0, 0, 0.12)"),er(G.LinearProgress,"primaryBg",Q(qp,l?w("palette-primary-main"):G.primary.main,.62)),er(G.LinearProgress,"secondaryBg",Q(qp,l?w("palette-secondary-main"):G.secondary.main,.62)),er(G.LinearProgress,"errorBg",Q(qp,l?w("palette-error-main"):G.error.main,.62)),er(G.LinearProgress,"infoBg",Q(qp,l?w("palette-info-main"):G.info.main,.62)),er(G.LinearProgress,"successBg",Q(qp,l?w("palette-success-main"):G.success.main,.62)),er(G.LinearProgress,"warningBg",Q(qp,l?w("palette-warning-light"):G.warning.main,.62)),er(G.Skeleton,"bg",M?Q(dte,l?w("palette-text-primary"):G.text.primary,.11):`rgba(${ee("palette-text-primaryChannel")} / 0.11)`),er(G.Slider,"primaryTrack",Q(qp,l?w("palette-primary-main"):G.primary.main,.62)),er(G.Slider,"secondaryTrack",Q(qp,l?w("palette-secondary-main"):G.secondary.main,.62)),er(G.Slider,"errorTrack",Q(qp,l?w("palette-error-main"):G.error.main,.62)),er(G.Slider,"infoTrack",Q(qp,l?w("palette-info-main"):G.info.main,.62)),er(G.Slider,"successTrack",Q(qp,l?w("palette-success-main"):G.success.main,.62)),er(G.Slider,"warningTrack",Q(qp,l?w("palette-warning-main"):G.warning.main,.62));const ie=M?Q(Up,l?w("palette-background-default"):G.background.default,.6825):EBe(G.background.default,.8);er(G.SnackbarContent,"bg",ie),er(G.SnackbarContent,"color",cP(()=>M?IOt.text.primary:G.getContrastText(ie))),er(G.SpeedDialAction,"fabHoverBg",EBe(G.background.paper,.15)),er(G.StepConnector,"border",ee("palette-grey-400")),er(G.StepContent,"border",ee("palette-grey-400")),er(G.Switch,"defaultColor",ee("palette-common-white")),er(G.Switch,"defaultDisabledColor",ee("palette-grey-100")),er(G.Switch,"primaryDisabledColor",Q(qp,l?w("palette-primary-main"):G.primary.main,.62)),er(G.Switch,"secondaryDisabledColor",Q(qp,l?w("palette-secondary-main"):G.secondary.main,.62)),er(G.Switch,"errorDisabledColor",Q(qp,l?w("palette-error-main"):G.error.main,.62)),er(G.Switch,"infoDisabledColor",Q(qp,l?w("palette-info-main"):G.info.main,.62)),er(G.Switch,"successDisabledColor",Q(qp,l?w("palette-success-main"):G.success.main,.62)),er(G.Switch,"warningDisabledColor",Q(qp,l?w("palette-warning-main"):G.warning.main,.62)),er(G.TableCell,"border",Q(qp,dte(l?w("palette-divider"):G.divider,1),.88)),er(G.Tooltip,"bg",Q(dte,l?w("palette-grey-700"):G.grey[700],.92))}if(G.mode==="dark"){er(G.Alert,"errorColor",Q(qp,l?w("palette-error-light"):G.error.light,.6)),er(G.Alert,"infoColor",Q(qp,l?w("palette-info-light"):G.info.light,.6)),er(G.Alert,"successColor",Q(qp,l?w("palette-success-light"):G.success.light,.6)),er(G.Alert,"warningColor",Q(qp,l?w("palette-warning-light"):G.warning.light,.6)),er(G.Alert,"errorFilledBg",ee("palette-error-dark")),er(G.Alert,"infoFilledBg",ee("palette-info-dark")),er(G.Alert,"successFilledBg",ee("palette-success-dark")),er(G.Alert,"warningFilledBg",ee("palette-warning-dark")),er(G.Alert,"errorFilledColor",cP(()=>G.getContrastText(G.error.dark))),er(G.Alert,"infoFilledColor",cP(()=>G.getContrastText(G.info.dark))),er(G.Alert,"successFilledColor",cP(()=>G.getContrastText(G.success.dark))),er(G.Alert,"warningFilledColor",cP(()=>G.getContrastText(G.warning.dark))),er(G.Alert,"errorStandardBg",Q(Up,l?w("palette-error-light"):G.error.light,.9)),er(G.Alert,"infoStandardBg",Q(Up,l?w("palette-info-light"):G.info.light,.9)),er(G.Alert,"successStandardBg",Q(Up,l?w("palette-success-light"):G.success.light,.9)),er(G.Alert,"warningStandardBg",Q(Up,l?w("palette-warning-light"):G.warning.light,.9)),er(G.Alert,"errorIconColor",ee("palette-error-main")),er(G.Alert,"infoIconColor",ee("palette-info-main")),er(G.Alert,"successIconColor",ee("palette-success-main")),er(G.Alert,"warningIconColor",ee("palette-warning-main")),er(G.AppBar,"defaultBg",ee("palette-grey-900")),er(G.AppBar,"darkBg",ee("palette-background-paper")),er(G.AppBar,"darkColor",ee("palette-text-primary")),er(G.Avatar,"defaultBg",ee("palette-grey-600")),er(G.Button,"inheritContainedBg",ee("palette-grey-800")),er(G.Button,"inheritContainedHoverBg",ee("palette-grey-700")),er(G.Chip,"defaultBorder",ee("palette-grey-700")),er(G.Chip,"defaultAvatarColor",ee("palette-grey-300")),er(G.Chip,"defaultIconColor",ee("palette-grey-300")),er(G.FilledInput,"bg","rgba(255, 255, 255, 0.09)"),er(G.FilledInput,"hoverBg","rgba(255, 255, 255, 0.13)"),er(G.FilledInput,"disabledBg","rgba(255, 255, 255, 0.12)"),er(G.LinearProgress,"primaryBg",Q(Up,l?w("palette-primary-main"):G.primary.main,.5)),er(G.LinearProgress,"secondaryBg",Q(Up,l?w("palette-secondary-main"):G.secondary.main,.5)),er(G.LinearProgress,"errorBg",Q(Up,l?w("palette-error-main"):G.error.main,.5)),er(G.LinearProgress,"infoBg",Q(Up,l?w("palette-info-main"):G.info.main,.5)),er(G.LinearProgress,"successBg",Q(Up,l?w("palette-success-main"):G.success.main,.5)),er(G.LinearProgress,"warningBg",Q(Up,l?w("palette-warning-main"):G.warning.main,.5)),er(G.Skeleton,"bg",M?Q(dte,l?w("palette-text-primary"):G.text.primary,.13):`rgba(${ee("palette-text-primaryChannel")} / 0.13)`),er(G.Slider,"primaryTrack",Q(Up,l?w("palette-primary-main"):G.primary.main,.5)),er(G.Slider,"secondaryTrack",Q(Up,l?w("palette-secondary-main"):G.secondary.main,.5)),er(G.Slider,"errorTrack",Q(Up,l?w("palette-error-main"):G.error.main,.5)),er(G.Slider,"infoTrack",Q(Up,l?w("palette-info-main"):G.info.main,.5)),er(G.Slider,"successTrack",Q(Up,l?w("palette-success-main"):G.success.main,.5)),er(G.Slider,"warningTrack",Q(Up,l?w("palette-warning-light"):G.warning.main,.5));const ie=M?Q(qp,l?w("palette-background-default"):G.background.default,.985):EBe(G.background.default,.98);er(G.SnackbarContent,"bg",ie),er(G.SnackbarContent,"color",cP(()=>M?xci.text.primary:G.getContrastText(ie))),er(G.SpeedDialAction,"fabHoverBg",EBe(G.background.paper,.15)),er(G.StepConnector,"border",ee("palette-grey-600")),er(G.StepContent,"border",ee("palette-grey-600")),er(G.Switch,"defaultColor",ee("palette-grey-300")),er(G.Switch,"defaultDisabledColor",ee("palette-grey-600")),er(G.Switch,"primaryDisabledColor",Q(Up,l?w("palette-primary-main"):G.primary.main,.55)),er(G.Switch,"secondaryDisabledColor",Q(Up,l?w("palette-secondary-main"):G.secondary.main,.55)),er(G.Switch,"errorDisabledColor",Q(Up,l?w("palette-error-main"):G.error.main,.55)),er(G.Switch,"infoDisabledColor",Q(Up,l?w("palette-info-main"):G.info.main,.55)),er(G.Switch,"successDisabledColor",Q(Up,l?w("palette-success-main"):G.success.main,.55)),er(G.Switch,"warningDisabledColor",Q(Up,l?w("palette-warning-light"):G.warning.main,.55)),er(G.TableCell,"border",Q(Up,dte(l?w("palette-divider"):G.divider,1),.68)),er(G.Tooltip,"bg",Q(dte,l?w("palette-grey-700"):G.grey[700],.92))}xH(G.background,"default"),xH(G.background,"paper"),xH(G.common,"background"),xH(G.common,"onBackground"),xH(G,"divider"),Object.keys(G).forEach(ie=>{const se=G[ie];ie!=="tonalOffset"&&se&&typeof se=="object"&&(se.main&&er(G[ie],"mainChannel",ske(oke(se.main))),se.light&&er(G[ie],"lightChannel",ske(oke(se.light))),se.dark&&er(G[ie],"darkChannel",ske(oke(se.dark))),se.contrastText&&er(G[ie],"contrastTextChannel",ske(oke(se.contrastText))),ie==="text"&&(xH(G[ie],"primary"),xH(G[ie],"secondary")),ie==="action"&&(se.active&&xH(G[ie],"active"),se.selected&&xH(G[ie],"selected")))})}),F=e.reduce((te,G)=>F_(te,G),F);const j={prefix:o,disableCssColorScheme:r,shouldSkipGeneratingVar:c,getSelector:Hhr(F),enableContrastVars:l},{vars:W,generateThemeVars:U,generateStyleSheets:Z}=$dr(F,j);return F.vars=W,Object.entries(F.colorSchemes[F.defaultColorScheme]).forEach(([te,G])=>{F[te]=G}),F.generateThemeVars=U,F.generateStyleSheets=Z,F.generateSpacing=function(){return aci(p.spacing,_et(this))},F.getColorSchemeSelector=zdr(d),F.spacing=F.generateSpacing(),F.shouldSkipGeneratingVar=c,F.unstable_sxConfig={...a5e,...p?.unstable_sxConfig},F.unstable_sx=function(G){return XK({sx:G,theme:this})},F.toRuntimeSource=Lci,F}function YNn(n,e,t){n.colorSchemes&&t&&(n.colorSchemes[e]={...t!==!0&&t,palette:Ozt({...t===!0?{}:t.palette,mode:e})})}function $1e(n={},...e){const{palette:t,cssVariables:i=!1,colorSchemes:r=t?void 0:{light:!0},defaultColorScheme:o=t?.mode,...l}=n,c=o||"light",d=r?.[c],h={...r,...t?{[c]:{...typeof d!="boolean"&&d,palette:t}}:void 0};if(i===!1){if(!("colorSchemes"in n))return AOt(n,...e);let p=t;"palette"in n||h[c]&&(h[c]!==!0?p=h[c].palette:c==="dark"&&(p={mode:"dark"}));const m=AOt({...n,palette:p},...e);return m.defaultColorScheme=c,m.colorSchemes=h,m.palette.mode==="light"&&(m.colorSchemes.light={...h.light!==!0&&h.light,palette:m.palette},YNn(m,"dark",h.dark)),m.palette.mode==="dark"&&(m.colorSchemes.dark={...h.dark!==!0&&h.dark,palette:m.palette},YNn(m,"light",h.light)),m}return!t&&!("light"in h)&&c==="light"&&(h.light=!0),$hr({...l,colorSchemes:h,defaultColorScheme:c,...typeof i!="boolean"&&i},...e)}const Fet=$1e(),v3="$$material";function Tf(){const n=ooe(Fet);return n[v3]||n}function zhr(n){return k.jsx(lci,{...n,defaultTheme:Fet,themeId:v3})}function jet(n){return n!=="ownerState"&&n!=="theme"&&n!=="sx"&&n!=="as"}const $_=n=>jet(n)&&n!=="classes",nn=fci({themeId:v3,defaultTheme:Fet,rootShouldForwardProp:$_});function Nzt(n){return function(t){return k.jsx(zhr,{styles:typeof n=="function"?i=>n({theme:i,...t}):n})}}function Uhr(){return Let}function Vo(n){return Tdr(n)}const MOt=typeof Nzt({})=="function",qhr=(n,e)=>({WebkitFontSmoothing:"antialiased",MozOsxFontSmoothing:"grayscale",boxSizing:"border-box",WebkitTextSizeAdjust:"100%",...e&&!n.vars&&{colorScheme:n.palette.mode}}),Ghr=n=>({color:(n.vars||n).palette.text.primary,...n.typography.body1,backgroundColor:(n.vars||n).palette.background.default,"@media print":{backgroundColor:(n.vars||n).palette.common.white}}),Aci=(n,e=!1)=>{const t={};e&&n.colorSchemes&&typeof n.getColorSchemeSelector=="function"&&Object.entries(n.colorSchemes).forEach(([o,l])=>{const c=n.getColorSchemeSelector(o);c.startsWith("@")?t[c]={":root":{colorScheme:l.palette?.mode}}:t[c.replace(/\s*&/,"")]={colorScheme:l.palette?.mode}});let i={html:qhr(n,e),"*, *::before, *::after":{boxSizing:"inherit"},"strong, b":{fontWeight:n.typography.fontWeightBold},body:{margin:0,...Ghr(n),"&::backdrop":{backgroundColor:(n.vars||n).palette.background.default}},...t};const r=n.components?.MuiCssBaseline?.styleOverrides;return r&&(i=[i,r]),i},gUe="mui-ecs",Khr=n=>{const e=Aci(n,!1),t=Array.isArray(e)?e[0]:e;return!n.vars&&t&&(t.html[`:root:has(${gUe})`]={colorScheme:n.palette.mode}),n.colorSchemes&&Object.entries(n.colorSchemes).forEach(([i,r])=>{const o=n.getColorSchemeSelector(i);o.startsWith("@")?t[o]={[`:root:not(:has(.${gUe}))`]:{colorScheme:r.palette?.mode}}:t[o.replace(/\s*&/,"")]={[`&:not(:has(.${gUe}))`]:{colorScheme:r.palette?.mode}}}),e},Yhr=Nzt(MOt?({theme:n,enableColorScheme:e})=>Aci(n,e):({theme:n})=>Khr(n));function Zhr(n){const e=Vo({props:n,name:"MuiCssBaseline"}),{children:t,enableColorScheme:i=!1}=e;return k.jsxs(L.Fragment,{children:[MOt&&k.jsx(Yhr,{enableColorScheme:i}),!MOt&&!i&&k.jsx("span",{className:gUe,style:{display:"none"}}),t]})}var Ws=function(){return Ws=Object.assign||function(e){for(var t,i=1,r=arguments.length;i=n.length&&(n=void 0),{value:n&&n[i++],done:!n}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")}function xf(n,e){var t=typeof Symbol=="function"&&n[Symbol.iterator];if(!t)return n;var i=t.call(n),r,o=[],l;try{for(;(e===void 0||e-- >0)&&!(r=i.next()).done;)o.push(r.value)}catch(c){l={error:c}}finally{try{r&&!r.done&&(t=i.return)&&t.call(i)}finally{if(l)throw l.error}}return o}function $m(n,e,t){if(arguments.length===2)for(var i=0,r=e.length,o;i"u"||typeof i=="string"||ume(i)?{target:i,event:n}:Ws(Ws({},i),{event:n})});return t}function pfr(n){if(!(n===void 0||n===nfr))return AR(n)}function Bci(n,e,t,i,r){var o=n.options.guards,l={state:r,cond:e,_event:i};if(e.type===POt)return(o?.[e.name]||e.predicate)(t,i.data,l);var c=o?.[e.type];if(!c)throw new Error("Guard '".concat(e.type,"' is not implemented on machine '").concat(n.id,"'."));return c(t,i.data,l)}function Wci(n){return typeof n=="string"?{type:n}:n}function n4e(n,e,t){var i=function(){},r=typeof n=="object",o=r?n:null;return{next:((r?n.next:n)||i).bind(o),error:((r?n.error:e)||i).bind(o),complete:((r?n.complete:t)||i).bind(o)}}function TBe(n,e){return"".concat(n,":invocation[").concat(e,"]")}function jOt(n){return(n.type===h5e||n.type===Het&&n.to===Qre.Internal)&&typeof n.delay!="number"}var Ype=W2({type:Qhr});function HOt(n,e){return e&&e[n]||void 0}function l6e(n,e){var t;if(Uf(n)||typeof n=="number"){var i=HOt(n,e);Yf(i)?t={type:n,exec:i}:i?t=i:t={type:n,exec:void 0}}else if(Yf(n))t={type:n.name||n.toString(),exec:n};else{var i=HOt(n.type,e);if(Yf(i))t=Ws(Ws({},n),{exec:i});else if(i){var r=i.type||n.type;t=Ws(Ws(Ws({},i),n),{type:r})}else t=n}return t}var yG=function(n,e){if(!n)return[];var t=z1e(n)?n:[n];return t.map(function(i){return l6e(i,e)})};function Bzt(n){var e=l6e(n);return Ws(Ws({id:Uf(n)?n:e.id},e),{type:e.type})}function Vci(n,e){return{type:h5e,event:typeof n=="function"?n:Bet(n),delay:e?e.delay:void 0,id:e?.id}}function gfr(n,e,t,i){var r={_event:t},o=W2(Yf(n.event)?n.event(e,t.data,r):n.event),l;if(Uf(n.delay)){var c=i&&i[n.delay];l=Yf(c)?c(e,t.data,r):c}else l=Yf(n.delay)?n.delay(e,t.data,r):n.delay;return Ws(Ws({},n),{type:h5e,_event:o,delay:l})}function f5e(n,e){return{to:e?e.to:void 0,type:Het,event:Yf(n)?n:Bet(n),delay:e?e.delay:void 0,id:e&&e.id!==void 0?e.id:Yf(n)?n.name:Pci(n)}}function mfr(n,e,t,i){var r={_event:t},o=W2(Yf(n.event)?n.event(e,t.data,r):n.event),l;if(Uf(n.delay)){var c=i&&i[n.delay];l=Yf(c)?c(e,t.data,r):c}else l=Yf(n.delay)?n.delay(e,t.data,r):n.delay;var d=Yf(n.to)?n.to(e,t.data,r):n.to;return Ws(Ws({},n),{to:d,_event:o,event:o.data,delay:l})}function bfr(n,e){return f5e(n,Ws(Ws({},e),{to:Qre.Parent}))}function vfr(n,e,t){return f5e(e,Ws(Ws({},t),{to:n}))}var wfr=function(n,e,t){return Ws(Ws({},n),{value:Uf(n.expr)?n.expr:n.expr(e,t.data,{_event:t})})},yfr=function(n){return{type:Rci,sendId:n}};function _fr(n){var e=Bzt(n);return{type:xp.Start,activity:e,exec:void 0}}function Cfr(n){var e=Yf(n)?n:Bzt(n);return{type:xp.Stop,activity:e,exec:void 0}}function Sfr(n,e,t){var i=Yf(n.activity)?n.activity(e,t.data):n.activity,r=typeof i=="string"?{id:i}:i,o={type:xp.Stop,activity:r};return o}var xfr=function(n){return{type:jzt,assignment:n}};function Efr(n,e){var t=e?"#".concat(e):"";return"".concat(xp.After,"(").concat(n,")").concat(t)}function LBe(n,e){var t="".concat(xp.DoneState,".").concat(n),i={type:t,data:e};return i.toString=function(){return t},i}function bUe(n,e){var t="".concat(xp.DoneInvoke,".").concat(n),i={type:t,data:e};return i.toString=function(){return t},i}function lke(n,e){var t="".concat(xp.ErrorPlatform,".").concat(n),i={type:t,data:e};return i.toString=function(){return t},i}function kfr(n){return{type:xp.Pure,get:n}}function Tfr(n,e){return f5e(function(t,i){return i},Ws(Ws({},e),{to:n}))}function Lfr(n){return{type:xp.Choose,conds:n}}var Dfr=function(n){var e,t,i=[];try{for(var r=yh(n),o=r.next();!o.done;o=r.next())for(var l=o.value,c=0;c0;){var h=r.shift();t=n.transition(t,h,d),i.forEach(function(p){return p.next(t)})}o=!1}},c=Ofr({id:e.id,send:function(h){r.push(h),l()},getSnapshot:function(){return t},subscribe:function(h,p,m){var b=n4e(h,p,m);return i.add(b),b.next(t),{unsubscribe:function(){i.delete(b)}}}}),d={parent:e.parent,self:c,id:e.id||"anonymous",observers:i};return t=n.start?n.start(d):t,c}var WOt={sync:!1,autoForward:!1},rb;(function(n){n[n.NotStarted=0]="NotStarted",n[n.Running=1]="Running",n[n.Stopped=2]="Stopped"})(rb||(rb={}));var Ufr=(function(){function n(e,t){t===void 0&&(t=n.defaultOptions);var i=this;this.machine=e,this.delayedEventsMap={},this.listeners=new Set,this.contextListeners=new Set,this.stopListeners=new Set,this.doneListeners=new Set,this.eventListeners=new Set,this.sendListeners=new Set,this.initialized=!1,this.status=rb.NotStarted,this.children=new Map,this.forwardTo=new Set,this._outgoingQueue=[],this.init=this.start,this.send=function(p,m){if(z1e(p))return i.batch(p),i.state;var b=W2(Bet(p,m));if(i.status===rb.Stopped)return i.state;if(i.status!==rb.Running&&!i.options.deferEvents)throw new Error('Event "'.concat(b.name,'" was sent to uninitialized service "').concat(i.machine.id,`". Make sure .start() is called for this service, or set { deferEvents: true } in the service options. +Event: `).concat(JSON.stringify(b.data)));return i.scheduler.schedule(function(){i.forward(b);var w=i._nextState(b);i.update(w,b)}),i._state},this.sendTo=function(p,m,b){var w=i.parent&&(m===Qre.Parent||i.parent.id===m),_=w?i.parent:Uf(m)?m===Qre.Internal?i:i.children.get(m)||Oxe.get(m):hfr(m)?m:void 0;if(!_){if(!w)throw new Error("Unable to send event to child '".concat(m,"' from service '").concat(i.id,"'."));return}if("machine"in _){if(i.status!==rb.Stopped||i.parent!==_||i.state.done){var x=Ws(Ws({},p),{name:p.name===Jhr?"".concat(lke(i.id)):p.name,origin:i.sessionId});!b&&i.machine.config.predictableActionArguments?i._outgoingQueue.push([_,x]):_.send(x)}}else!b&&i.machine.config.predictableActionArguments?i._outgoingQueue.push([_,p.data]):_.send(p.data)},this._exec=function(p,m,b,w){w===void 0&&(w=i.machine.options.actions);var _=p.exec||HOt(p.type,w),x=Yf(_)?_:_?_.exec:p.exec;if(x)try{return x(m,b.data,i.machine.config.predictableActionArguments?{action:p,_event:b}:{action:p,state:i.state,_event:b})}catch(Q){throw i.parent&&i.parent.send({type:"xstate.error",data:Q}),Q}switch(p.type){case h5e:{var T=p;i.defer(T);break}case Het:var I=p;if(typeof I.delay=="number"){i.defer(I);return}else I.to?i.sendTo(I._event,I.to,b===Ype):i.send(I._event);break;case Rci:i.cancel(p.sendId);break;case OOt:{if(i.status!==rb.Running)return;var D=p.activity;if(!i.machine.config.predictableActionArguments&&!i.state.activities[D.id||D.type])break;if(D.type===xp.Invoke){var A=Wci(D.src),M=i.machine.options.services?i.machine.options.services[A.type]:void 0,O=D.id,F=D.data,j="autoForward"in D?D.autoForward:!!D.forward;if(!M)return;var W=F?Xqe(F,m,b):void 0;if(typeof M=="string")return;var U=Yf(M)?M(m,b.data,{data:W,src:A,meta:D.meta}):M;if(!U)return;var Z=void 0;ume(U)&&(U=W?U.withContext(W):U,Z={autoForward:j}),i.spawn(U,O,Z)}else i.spawnActivity(D);break}case Fzt:{i.stopChild(p.activity.id);break}case Mci:var te=p,G=te.label,ee=te.value;G?i.logger(G,ee):i.logger(ee);break}};var r=Ws(Ws({},n.defaultOptions),t),o=r.clock,l=r.logger,c=r.parent,d=r.id,h=d!==void 0?d:e.id;this.id=h,this.logger=l,this.clock=o,this.parent=c,this.options=r,this.scheduler=new nPn({deferEvents:this.options.deferEvents}),this.sessionId=Oxe.bookId()}return Object.defineProperty(n.prototype,"initialState",{get:function(){var e=this;return this._initialState?this._initialState:dfe(this,function(){return e._initialState=e.machine.initialState,e._initialState})},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"state",{get:function(){return this._state},enumerable:!1,configurable:!0}),n.prototype.execute=function(e,t){var i,r;try{for(var o=yh(e.actions),l=o.next();!l.done;l=o.next()){var c=l.value;this.exec(c,e,t)}}catch(d){i={error:d}}finally{try{l&&!l.done&&(r=o.return)&&r.call(o)}finally{if(i)throw i.error}}},n.prototype.update=function(e,t){var i,r,o,l,c,d,h,p,m=this;if(e._sessionid=this.sessionId,this._state=e,(!this.machine.config.predictableActionArguments||t===Ype)&&this.options.execute)this.execute(this.state);else for(var b=void 0;b=this._outgoingQueue.shift();)b[0].send(b[1]);if(this.children.forEach(function(U){m.state.children[U.id]=U}),this.devTools&&this.devTools.send(t.data,e),e.event)try{for(var w=yh(this.eventListeners),_=w.next();!_.done;_=w.next()){var x=_.value;x(e.event)}}catch(U){i={error:U}}finally{try{_&&!_.done&&(r=w.return)&&r.call(w)}finally{if(i)throw i.error}}try{for(var T=yh(this.listeners),I=T.next();!I.done;I=T.next()){var x=I.value;x(e,e.event)}}catch(U){o={error:U}}finally{try{I&&!I.done&&(l=T.return)&&l.call(T)}finally{if(o)throw o.error}}try{for(var D=yh(this.contextListeners),A=D.next();!A.done;A=D.next()){var M=A.value;M(this.state.context,this.state.history?this.state.history.context:void 0)}}catch(U){c={error:U}}finally{try{A&&!A.done&&(d=D.return)&&d.call(D)}finally{if(c)throw c.error}}if(this.state.done){var O=e.configuration.find(function(U){return U.type==="final"&&U.parent===m.machine}),F=O&&O.doneData?Xqe(O.doneData,e.context,t):void 0;this._doneEvent=bUe(this.id,F);try{for(var j=yh(this.doneListeners),W=j.next();!W.done;W=j.next()){var x=W.value;x(this._doneEvent)}}catch(U){h={error:U}}finally{try{W&&!W.done&&(p=j.return)&&p.call(j)}finally{if(h)throw h.error}}this._stop(),this._stopChildren(),Oxe.free(this.sessionId)}},n.prototype.onTransition=function(e){return this.listeners.add(e),this.status===rb.Running&&e(this.state,this.state.event),this},n.prototype.subscribe=function(e,t,i){var r=this,o=n4e(e,t,i);this.listeners.add(o.next),this.status!==rb.NotStarted&&o.next(this.state);var l=function(){r.doneListeners.delete(l),r.stopListeners.delete(l),o.complete()};return this.status===rb.Stopped?o.complete():(this.onDone(l),this.onStop(l)),{unsubscribe:function(){r.listeners.delete(o.next),r.doneListeners.delete(l),r.stopListeners.delete(l)}}},n.prototype.onEvent=function(e){return this.eventListeners.add(e),this},n.prototype.onSend=function(e){return this.sendListeners.add(e),this},n.prototype.onChange=function(e){return this.contextListeners.add(e),this},n.prototype.onStop=function(e){return this.stopListeners.add(e),this},n.prototype.onDone=function(e){return this.status===rb.Stopped&&this._doneEvent?e(this._doneEvent):this.doneListeners.add(e),this},n.prototype.off=function(e){return this.listeners.delete(e),this.eventListeners.delete(e),this.sendListeners.delete(e),this.stopListeners.delete(e),this.doneListeners.delete(e),this.contextListeners.delete(e),this},n.prototype.start=function(e){var t=this;if(this.status===rb.Running)return this;this.machine._init(),Oxe.register(this.sessionId,this),this.initialized=!0,this.status=rb.Running;var i=e===void 0?this.initialState:dfe(this,function(){return jfr(e)?t.machine.resolveState(e):t.machine.resolveState(hL.from(e,t.machine.context))});return this.options.devTools&&this.attachDev(),this.scheduler.initialize(function(){t.update(i,Ype)}),this},n.prototype._stopChildren=function(){this.children.forEach(function(e){Yf(e.stop)&&e.stop()}),this.children.clear()},n.prototype._stop=function(){var e,t,i,r,o,l,c,d,h,p;try{for(var m=yh(this.listeners),b=m.next();!b.done;b=m.next()){var w=b.value;this.listeners.delete(w)}}catch(j){e={error:j}}finally{try{b&&!b.done&&(t=m.return)&&t.call(m)}finally{if(e)throw e.error}}try{for(var _=yh(this.stopListeners),x=_.next();!x.done;x=_.next()){var w=x.value;w(),this.stopListeners.delete(w)}}catch(j){i={error:j}}finally{try{x&&!x.done&&(r=_.return)&&r.call(_)}finally{if(i)throw i.error}}try{for(var T=yh(this.contextListeners),I=T.next();!I.done;I=T.next()){var w=I.value;this.contextListeners.delete(w)}}catch(j){o={error:j}}finally{try{I&&!I.done&&(l=T.return)&&l.call(T)}finally{if(o)throw o.error}}try{for(var D=yh(this.doneListeners),A=D.next();!A.done;A=D.next()){var w=A.value;this.doneListeners.delete(w)}}catch(j){c={error:j}}finally{try{A&&!A.done&&(d=D.return)&&d.call(D)}finally{if(c)throw c.error}}if(!this.initialized)return this;this.initialized=!1,this.status=rb.Stopped,this._initialState=void 0;try{for(var M=yh(Object.keys(this.delayedEventsMap)),O=M.next();!O.done;O=M.next()){var F=O.value;this.clock.clearTimeout(this.delayedEventsMap[F])}}catch(j){h={error:j}}finally{try{O&&!O.done&&(p=M.return)&&p.call(M)}finally{if(h)throw h.error}}this.scheduler.clear(),this.scheduler=new nPn({deferEvents:this.options.deferEvents})},n.prototype.stop=function(){var e=this,t=this.scheduler;return this._stop(),t.schedule(function(){var i;if(!(!((i=e._state)===null||i===void 0)&&i.done)){var r=W2({type:"xstate.stop"}),o=dfe(e,function(){var l=f0($m([],xf(e.state.configuration),!1).sort(function(m,b){return b.order-m.order}).map(function(m){return yG(m.onExit,e.machine.options.actions)})),c=xf(Qqe(e.machine,e.state,e.state.context,r,[{type:"exit",actions:l}],e.machine.config.predictableActionArguments?e._exec:void 0,e.machine.config.predictableActionArguments||e.machine.config.preserveActionOrder),2),d=c[0],h=c[1],p=new hL({value:e.state.value,context:h,_event:r,_sessionid:e.sessionId,historyValue:void 0,history:e.state,actions:d.filter(function(m){return!jOt(m)}),activities:{},events:[],configuration:[],transitions:[],children:{},done:e.state.done,tags:e.state.tags,machine:e.machine});return p.changed=!0,p});e.update(o,r),e._stopChildren(),Oxe.free(e.sessionId)}}),this},n.prototype.batch=function(e){var t=this;if(!(this.status===rb.NotStarted&&this.options.deferEvents)){if(this.status!==rb.Running)throw new Error("".concat(e.length,' event(s) were sent to uninitialized service "').concat(this.machine.id,'". Make sure .start() is called for this service, or set { deferEvents: true } in the service options.'))}if(e.length){var i=!!this.machine.config.predictableActionArguments&&this._exec;this.scheduler.schedule(function(){var r,o,l=t.state,c=!1,d=[],h=function(w){var _=W2(w);t.forward(_),l=dfe(t,function(){return t.machine.transition(l,_,void 0,i||void 0)}),d.push.apply(d,$m([],xf(t.machine.config.predictableActionArguments?l.actions:l.actions.map(function(x){return Hfr(x,l)})),!1)),c=c||!!l.changed};try{for(var p=yh(e),m=p.next();!m.done;m=p.next()){var b=m.value;h(b)}}catch(w){r={error:w}}finally{try{m&&!m.done&&(o=p.return)&&o.call(p)}finally{if(r)throw r.error}}l.changed=c,l.actions=d,t.update(l,W2(e[e.length-1]))})}},n.prototype.sender=function(e){return this.send.bind(this,e)},n.prototype._nextState=function(e,t){var i=this;t===void 0&&(t=!!this.machine.config.predictableActionArguments&&this._exec);var r=W2(e);if(r.name.indexOf(ZNn)===0&&!this.state.nextEvents.some(function(l){return l.indexOf(ZNn)===0}))throw r.data.data;var o=dfe(this,function(){return i.machine.transition(i.state,r,void 0,t||void 0)});return o},n.prototype.nextState=function(e){return this._nextState(e,!1)},n.prototype.forward=function(e){var t,i;try{for(var r=yh(this.forwardTo),o=r.next();!o.done;o=r.next()){var l=o.value,c=this.children.get(l);if(!c)throw new Error("Unable to forward event '".concat(e,"' from interpreter '").concat(this.id,"' to nonexistant child '").concat(l,"'."));c.send(e)}}catch(d){t={error:d}}finally{try{o&&!o.done&&(i=r.return)&&i.call(r)}finally{if(t)throw t.error}}},n.prototype.defer=function(e){var t=this,i=this.clock.setTimeout(function(){"to"in e&&e.to?t.sendTo(e._event,e.to,!0):t.send(e._event)},e.delay);e.id&&(this.delayedEventsMap[e.id]=i)},n.prototype.cancel=function(e){this.clock.clearTimeout(this.delayedEventsMap[e]),delete this.delayedEventsMap[e]},n.prototype.exec=function(e,t,i){i===void 0&&(i=this.machine.options.actions),this._exec(e,t.context,t._event,i)},n.prototype.removeChild=function(e){var t;this.children.delete(e),this.forwardTo.delete(e),(t=this.state)===null||t===void 0||delete t.children[e]},n.prototype.stopChild=function(e){var t=this.children.get(e);t&&(this.removeChild(e),Yf(t.stop)&&t.stop())},n.prototype.spawn=function(e,t,i){if(this.status!==rb.Running)return Wzt(e,t);if(JNn(e))return this.spawnPromise(Promise.resolve(e),t);if(Yf(e))return this.spawnCallback(e,t);if(Mfr(e))return this.spawnActor(e,t);if(dfr(e))return this.spawnObservable(e,t);if(ume(e))return this.spawnMachine(e,Ws(Ws({},i),{id:t}));if(afr(e))return this.spawnBehavior(e,t);throw new Error('Unable to spawn entity "'.concat(t,'" of type "').concat(typeof e,'".'))},n.prototype.spawnMachine=function(e,t){var i=this;t===void 0&&(t={});var r=new n(e,Ws(Ws({},this.options),{parent:this,id:t.id||e.id})),o=Ws(Ws({},WOt),t);o.sync&&r.onTransition(function(c){i.send(Oci,{state:c,id:r.id})});var l=r;return this.children.set(r.id,l),o.autoForward&&this.forwardTo.add(r.id),r.onDone(function(c){i.removeChild(r.id),i.send(W2(c,{origin:r.id}))}).start(),l},n.prototype.spawnBehavior=function(e,t){var i=zfr(e,{id:t,parent:this});return this.children.set(t,i),i},n.prototype.spawnPromise=function(e,t){var i,r=this,o=!1,l;e.then(function(d){o||(l=d,r.removeChild(t),r.send(W2(bUe(t,d),{origin:t})))},function(d){if(!o){r.removeChild(t);var h=lke(t,d);try{r.send(W2(h,{origin:t}))}catch{r.devTools&&r.devTools.send(h,r.state),r.machine.strict&&r.stop()}}});var c=(i={id:t,send:function(){},subscribe:function(d,h,p){var m=n4e(d,h,p),b=!1;return e.then(function(w){b||(m.next(w),!b&&m.complete())},function(w){b||m.error(w)}),{unsubscribe:function(){return b=!0}}},stop:function(){o=!0},toJSON:function(){return{id:t}},getSnapshot:function(){return l}},i[wG]=function(){return this},i);return this.children.set(t,c),c},n.prototype.spawnCallback=function(e,t){var i,r=this,o=!1,l=new Set,c=new Set,d,h=function(b){d=b,c.forEach(function(w){return w(b)}),!o&&r.send(W2(b,{origin:t}))},p;try{p=e(h,function(b){l.add(b)})}catch(b){this.send(lke(t,b))}if(JNn(p))return this.spawnPromise(p,t);var m=(i={id:t,send:function(b){return l.forEach(function(w){return w(b)})},subscribe:function(b){var w=n4e(b);return c.add(w.next),{unsubscribe:function(){c.delete(w.next)}}},stop:function(){o=!0,Yf(p)&&p()},toJSON:function(){return{id:t}},getSnapshot:function(){return d}},i[wG]=function(){return this},i);return this.children.set(t,m),m},n.prototype.spawnObservable=function(e,t){var i,r=this,o,l=e.subscribe(function(d){o=d,r.send(W2(d,{origin:t}))},function(d){r.removeChild(t),r.send(W2(lke(t,d),{origin:t}))},function(){r.removeChild(t),r.send(W2(bUe(t),{origin:t}))}),c=(i={id:t,send:function(){},subscribe:function(d,h,p){return e.subscribe(d,h,p)},stop:function(){return l.unsubscribe()},getSnapshot:function(){return o},toJSON:function(){return{id:t}}},i[wG]=function(){return this},i);return this.children.set(t,c),c},n.prototype.spawnActor=function(e,t){return this.children.set(t,e),e},n.prototype.spawnActivity=function(e){var t=this.machine.options&&this.machine.options.activities?this.machine.options.activities[e.type]:void 0;if(t){var i=t(this.state.context,e);this.spawnEffect(e.id,i)}},n.prototype.spawnEffect=function(e,t){var i;this.children.set(e,(i={id:e,send:function(){},subscribe:function(){return{unsubscribe:function(){}}},stop:t||void 0,getSnapshot:function(){},toJSON:function(){return{id:e}}},i[wG]=function(){return this},i))},n.prototype.attachDev=function(){var e=Vzt();if(this.options.devTools&&e){if(e.__REDUX_DEVTOOLS_EXTENSION__){var t=typeof this.options.devTools=="object"?this.options.devTools:void 0;this.devTools=e.__REDUX_DEVTOOLS_EXTENSION__.connect(Ws(Ws({name:this.id,autoPause:!0,stateSanitizer:function(i){return{value:i.value,context:i.context,actions:i.actions}}},t),{features:Ws({jump:!1,skip:!1},t?t.features:void 0)}),this.machine),this.devTools.init(this.state)}$fr(this)}},n.prototype.toJSON=function(){return{id:this.id}},n.prototype[wG]=function(){return this},n.prototype.getSnapshot=function(){return this.status===rb.NotStarted?this.initialState:this._state},n.defaultOptions={execute:!0,deferEvents:!0,clock:{setTimeout:function(e,t){return setTimeout(e,t)},clearTimeout:function(e){return clearTimeout(e)}},logger:console.log.bind(console),devTools:!1},n.interpret=Kci,n})(),qfr=function(n){return Uf(n)?Ws(Ws({},WOt),{name:n}):Ws(Ws(Ws({},WOt),{name:ffr()}),n)};function u6e(n,e){var t=qfr(e);return Ifr(function(i){return i?i.spawn(n,t.name,t):Wzt(n,t.name)})}function Kci(n,e){var t=new Ufr(n,e);return t}function Gfr(n){if(typeof n=="string"){var e={type:n};return e.toString=function(){return n},e}return n}function DBe(n){return Ws(Ws({type:NOt},n),{toJSON:function(){n.onDone,n.onError;var e=Pzt(n,["onDone","onError"]);return Ws(Ws({},e),{type:NOt,src:Gfr(n.src)})}})}var IBe="",VOt="#",AEt="*",ihe={},rhe=function(n){return n[0]===VOt},Kfr=function(){return{actions:{},guards:{},services:{},activities:{},delays:{}}},Yfr=(function(){function n(e,t,i,r){i===void 0&&(i="context"in e?e.context:void 0);var o=this,l;this.config=e,this._context=i,this.order=-1,this.__xstatenode=!0,this.__cache={events:void 0,relativeValue:new Map,initialStateValue:void 0,initialState:void 0,on:void 0,transitions:void 0,candidates:{},delayedTransitions:void 0},this.idMap={},this.tags=[],this.options=Object.assign(Kfr(),t),this.parent=r?.parent,this.key=this.config.key||r?.key||this.config.id||"(machine)",this.machine=this.parent?this.parent.machine:this,this.path=this.parent?this.parent.path.concat(this.key):[],this.delimiter=this.config.delimiter||(this.parent?this.parent.delimiter:Nci),this.id=this.config.id||$m([this.machine.key],xf(this.path),!1).join(this.delimiter),this.version=this.parent?this.parent.version:this.config.version,this.type=this.config.type||(this.config.parallel?"parallel":this.config.states&&Object.keys(this.config.states).length?"compound":this.config.history?"history":"atomic"),this.schema=this.parent?this.machine.schema:(l=this.config.schema)!==null&&l!==void 0?l:{},this.description=this.config.description,this.initial=this.config.initial,this.states=this.config.states?ake(this.config.states,function(h,p){var m,b=new n(h,{},void 0,{parent:o,key:p});return Object.assign(o.idMap,Ws((m={},m[b.id]=b,m),b.idMap)),b}):ihe;var c=0;function d(h){var p,m;h.order=c++;try{for(var b=yh(zci(h)),w=b.next();!w.done;w=b.next()){var _=w.value;d(_)}}catch(x){p={error:x}}finally{try{w&&!w.done&&(m=b.return)&&m.call(b)}finally{if(p)throw p.error}}}d(this),this.history=this.config.history===!0?"shallow":this.config.history||!1,this._transient=!!this.config.always||(this.config.on?Array.isArray(this.config.on)?this.config.on.some(function(h){var p=h.event;return p===IBe}):IBe in this.config.on:!1),this.strict=!!this.config.strict,this.onEntry=AR(this.config.entry||this.config.onEntry).map(function(h){return l6e(h)}),this.onExit=AR(this.config.exit||this.config.onExit).map(function(h){return l6e(h)}),this.meta=this.config.meta,this.doneData=this.type==="final"?this.config.data:void 0,this.invoke=AR(this.config.invoke).map(function(h,p){var m,b;if(ume(h)){var w=TBe(o.id,p);return o.machine.options.services=Ws((m={},m[w]=h,m),o.machine.options.services),DBe({src:w,id:w})}else if(Uf(h.src)){var w=h.id||TBe(o.id,p);return DBe(Ws(Ws({},h),{id:w,src:h.src}))}else if(ume(h.src)||Yf(h.src)){var w=h.id||TBe(o.id,p);return o.machine.options.services=Ws((b={},b[w]=h.src,b),o.machine.options.services),DBe(Ws(Ws({id:w},h),{src:w}))}else{var _=h.src;return DBe(Ws(Ws({id:TBe(o.id,p)},h),{src:_}))}}),this.activities=AR(this.config.activities).concat(this.invoke).map(function(h){return Bzt(h)}),this.transition=this.transition.bind(this),this.tags=AR(this.config.tags)}return n.prototype._init=function(){this.__cache.transitions||Uci(this).forEach(function(e){return e.on})},n.prototype.withConfig=function(e,t){var i=this.options,r=i.actions,o=i.activities,l=i.guards,c=i.services,d=i.delays;return new n(this.config,{actions:Ws(Ws({},r),e.actions),activities:Ws(Ws({},o),e.activities),guards:Ws(Ws({},l),e.guards),services:Ws(Ws({},c),e.services),delays:Ws(Ws({},d),e.delays)},t??this.context)},n.prototype.withContext=function(e){return new n(this.config,this.options,e)},Object.defineProperty(n.prototype,"context",{get:function(){return Yf(this._context)?this._context():this._context},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"definition",{get:function(){return{id:this.id,key:this.key,version:this.version,context:this.context,type:this.type,initial:this.initial,history:this.history,states:ake(this.states,function(e){return e.definition}),on:this.on,transitions:this.transitions,entry:this.onEntry,exit:this.onExit,activities:this.activities||[],meta:this.meta,order:this.order||-1,data:this.doneData,invoke:this.invoke,description:this.description,tags:this.tags}},enumerable:!1,configurable:!0}),n.prototype.toJSON=function(){return this.definition},Object.defineProperty(n.prototype,"on",{get:function(){if(this.__cache.on)return this.__cache.on;var e=this.transitions;return this.__cache.on=e.reduce(function(t,i){return t[i.eventType]=t[i.eventType]||[],t[i.eventType].push(i),t},{})},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"after",{get:function(){return this.__cache.delayedTransitions||(this.__cache.delayedTransitions=this.getDelayedTransitions(),this.__cache.delayedTransitions)},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"transitions",{get:function(){return this.__cache.transitions||(this.__cache.transitions=this.formatTransitions(),this.__cache.transitions)},enumerable:!1,configurable:!0}),n.prototype.getCandidates=function(e){if(this.__cache.candidates[e])return this.__cache.candidates[e];var t=e===IBe,i=this.transitions.filter(function(r){var o=r.eventType===e;return t?o:o||r.eventType===AEt});return this.__cache.candidates[e]=i,i},n.prototype.getDelayedTransitions=function(){var e=this,t=this.config.after;if(!t)return[];var i=function(o,l){var c=Yf(o)?"".concat(e.id,":delay[").concat(l,"]"):o,d=Efr(c,e.id);return e.onEntry.push(f5e(d,{delay:o})),e.onExit.push(yfr(d)),d},r=z1e(t)?t.map(function(o,l){var c=i(o.delay,l);return Ws(Ws({},o),{event:c})}):f0(Object.keys(t).map(function(o,l){var c=t[o],d=Uf(c)?{target:c}:c,h=isNaN(+o)?o:+o,p=i(h,l);return AR(d).map(function(m){return Ws(Ws({},m),{event:p,delay:h})})}));return r.map(function(o){var l=o.delay;return Ws(Ws({},e.formatTransition(o)),{delay:l})})},n.prototype.getStateNodes=function(e){var t,i=this;if(!e)return[];var r=e instanceof hL?e.value:t4e(e,this.delimiter);if(Uf(r)){var o=this.getStateNode(r).initial;return o!==void 0?this.getStateNodes((t={},t[r]=o,t)):[this,this.states[r]]}var l=Object.keys(r),c=[this];return c.push.apply(c,$m([],xf(f0(l.map(function(d){return i.getStateNode(d).getStateNodes(r[d])}))),!1)),c},n.prototype.handles=function(e){var t=Pci(e);return this.events.includes(t)},n.prototype.resolveState=function(e){var t=e instanceof hL?e:hL.create(e),i=Array.from(cke([],this.getStateNodes(t.value)));return new hL(Ws(Ws({},t),{value:this.resolve(t.value),configuration:i,done:vUe(i,this),tags:tPn(i),machine:this.machine}))},n.prototype.transitionLeafNode=function(e,t,i){var r=this.getStateNode(e),o=r.next(t,i);return!o||!o.transitions.length?this.next(t,i):o},n.prototype.transitionCompoundNode=function(e,t,i){var r=Object.keys(e),o=this.getStateNode(r[0]),l=o._transition(e[r[0]],t,i);return!l||!l.transitions.length?this.next(t,i):l},n.prototype.transitionParallelNode=function(e,t,i){var r,o,l={};try{for(var c=yh(Object.keys(e)),d=c.next();!d.done;d=c.next()){var h=d.value,p=e[h];if(p){var m=this.getStateNode(h),b=m._transition(p,t,i);b&&(l[h]=b)}}}catch(I){r={error:I}}finally{try{d&&!d.done&&(o=c.return)&&o.call(c)}finally{if(r)throw r.error}}var w=Object.keys(l).map(function(I){return l[I]}),_=f0(w.map(function(I){return I.transitions})),x=w.some(function(I){return I.transitions.length>0});if(!x)return this.next(t,i);var T=f0(Object.keys(l).map(function(I){return l[I].configuration}));return{transitions:_,exitSet:f0(w.map(function(I){return I.exitSet})),configuration:T,source:t,actions:f0(Object.keys(l).map(function(I){return l[I].actions}))}},n.prototype._transition=function(e,t,i){return Uf(e)?this.transitionLeafNode(e,t,i):Object.keys(e).length===1?this.transitionCompoundNode(e,t,i):this.transitionParallelNode(e,t,i)},n.prototype.getTransitionData=function(e,t){return this._transition(e.value,e,W2(t))},n.prototype.next=function(e,t){var i,r,o=this,l=t.name,c=[],d=[],h;try{for(var p=yh(this.getCandidates(l)),m=p.next();!m.done;m=p.next()){var b=m.value,w=b.cond,_=b.in,x=e.context,T=_?Uf(_)&&rhe(_)?e.matches(t4e(this.getStateNodeById(_).path,this.delimiter)):Hzt(t4e(_,this.delimiter),rfr(this.path.slice(0,-2))(e.value)):!0,I=!1;try{I=!w||Bci(this.machine,w,x,t,e)}catch(M){throw new Error("Unable to evaluate guard '".concat(w.name||w.type,"' in transition for event '").concat(l,"' in state node '").concat(this.id,`': +`).concat(M.message))}if(I&&T){b.target!==void 0&&(d=b.target),c.push.apply(c,$m([],xf(b.actions),!1)),h=b;break}}}catch(M){i={error:M}}finally{try{m&&!m.done&&(r=p.return)&&r.call(p)}finally{if(i)throw i.error}}if(h){if(!d.length)return{transitions:[h],exitSet:[],configuration:e.value?[this]:[],source:e,actions:c};var D=f0(d.map(function(M){return o.getRelativeStateNodes(M,e.historyValue)})),A=!!h.internal;return{transitions:[h],exitSet:A?[]:f0(d.map(function(M){return o.getPotentiallyReenteringNodes(M)})),configuration:D,source:e,actions:c}}},n.prototype.getPotentiallyReenteringNodes=function(e){if(this.order0,w=b?e.configuration:t?t.configuration:[],_=vUe(w,this),x=b?Nfr(this.machine,m):void 0,T=t?t.historyValue?t.historyValue:e.source?this.machine.historyValue(t.value):void 0:void 0,I=this.getActions(new Set(w),_,e,i,o,t,r),D=t?Ws({},t.activities):{};try{for(var A=yh(I),M=A.next();!M.done;M=A.next()){var O=M.value;try{for(var F=(d=void 0,yh(O.actions)),j=F.next();!j.done;j=F.next()){var W=j.value;W.type===OOt?D[W.activity.id||W.activity.type]=W:W.type===Fzt&&(D[W.activity.id||W.activity.type]=!1)}}catch(pe){d={error:pe}}finally{try{j&&!j.done&&(h=F.return)&&h.call(F)}finally{if(d)throw d.error}}}}catch(pe){l={error:pe}}finally{try{M&&!M.done&&(c=A.return)&&c.call(A)}finally{if(l)throw l.error}}var U=xf(Qqe(this,t,i,o,I,r,this.machine.config.predictableActionArguments||this.machine.config.preserveActionOrder),2),Z=U[0],te=U[1],G=xf(lfr(Z,jOt),2),ee=G[0],Q=G[1],ie=Z.filter(function(pe){var me;return pe.type===OOt&&((me=pe.activity)===null||me===void 0?void 0:me.type)===NOt}),se=ie.reduce(function(pe,me){return pe[me.activity.id]=Afr(me.activity,p.machine,te,o),pe},t?Ws({},t.children):{}),ue=new hL({value:x||t.value,context:te,_event:o,_sessionid:t?t._sessionid:null,historyValue:x?T?cfr(T,x):void 0:t?t.historyValue:void 0,history:!x||e.source?t:void 0,actions:x?Q:[],activities:x?D:t?t.activities:{},events:[],configuration:w,transitions:e.transitions,children:se,done:_,tags:tPn(w),machine:this}),ne=i!==te;ue.changed=o.name===Oci||ne;var we=ue.history;we&&delete we.history;var de=!_&&(this._transient||m.some(function(pe){return pe._transient}));if(!b&&(!de||o.name===IBe))return ue;var ce=ue;if(!_)for(de&&(ce=this.resolveRaisedTransition(ce,{type:Xhr},o,r));ee.length;){var ye=ee.shift();ce=this.resolveRaisedTransition(ce,ye._event,o,r)}var he=ce.changed||(we?!!ce.actions.length||ne||typeof we.value!=typeof ce.value||!Gci(ce.value,we.value):void 0);return ce.changed=he,ce.history=we,ce},n.prototype.getStateNode=function(e){if(rhe(e))return this.machine.getStateNodeById(e);if(!this.states)throw new Error("Unable to retrieve child state '".concat(e,"' from '").concat(this.id,"'; no child states exist."));var t=this.states[e];if(!t)throw new Error("Child state '".concat(e,"' does not exist on '").concat(this.id,"'"));return t},n.prototype.getStateNodeById=function(e){var t=rhe(e)?e.slice(VOt.length):e;if(t===this.id)return this;var i=this.machine.idMap[t];if(!i)throw new Error("Child state node '#".concat(t,"' does not exist on machine '").concat(this.id,"'"));return i},n.prototype.getStateNodeByPath=function(e){if(typeof e=="string"&&rhe(e))try{return this.getStateNodeById(e.slice(1))}catch{}for(var t=FOt(e,this.delimiter).slice(),i=this;t.length;){var r=t.shift();if(!r.length)break;i=i.getStateNode(r)}return i},n.prototype.resolve=function(e){var t,i=this;if(!e)return this.initialStateValue||ihe;switch(this.type){case"parallel":return ake(this.initialStateValue,function(o,l){return o?i.getStateNode(l).resolve(e[l]||o):ihe});case"compound":if(Uf(e)){var r=this.getStateNode(e);return r.type==="parallel"||r.type==="compound"?(t={},t[e]=r.initialStateValue,t):e}return Object.keys(e).length?ake(e,function(o,l){return o?i.getStateNode(l).resolve(o):ihe}):this.initialStateValue||{};default:return e||ihe}},n.prototype.getResolvedPath=function(e){if(rhe(e)){var t=this.machine.idMap[e.slice(VOt.length)];if(!t)throw new Error("Unable to find state node '".concat(e,"'"));return t.path}return FOt(e,this.delimiter)},Object.defineProperty(n.prototype,"initialStateValue",{get:function(){var e;if(this.__cache.initialStateValue)return this.__cache.initialStateValue;var t;if(this.type==="parallel")t=QNn(this.states,function(i){return i.initialStateValue||ihe},function(i){return i.type!=="history"});else if(this.initial!==void 0){if(!this.states[this.initial])throw new Error("Initial state '".concat(this.initial,"' not found on '").concat(this.key,"'"));t=eGe(this.states[this.initial])?this.initial:(e={},e[this.initial]=this.states[this.initial].initialStateValue,e)}else t={};return this.__cache.initialStateValue=t,this.__cache.initialStateValue},enumerable:!1,configurable:!0}),n.prototype.getInitialState=function(e,t){this._init();var i=this.getStateNodes(e);return this.resolveTransition({configuration:i,exitSet:[],transitions:[],source:void 0,actions:[]},void 0,t??this.machine.context,void 0)},Object.defineProperty(n.prototype,"initialState",{get:function(){var e=this.initialStateValue;if(!e)throw new Error("Cannot retrieve initial state from simple state '".concat(this.id,"'."));return this.getInitialState(e)},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"target",{get:function(){var e;if(this.type==="history"){var t=this.config;Uf(t.target)?e=rhe(t.target)?Zqe(this.machine.getStateNodeById(t.target).path.slice(this.path.length-1)):t.target:e=t.target}return e},enumerable:!1,configurable:!0}),n.prototype.getRelativeStateNodes=function(e,t,i){return i===void 0&&(i=!0),i?e.type==="history"?e.resolveHistory(t):e.initialStateNodes:[e]},Object.defineProperty(n.prototype,"initialStateNodes",{get:function(){var e=this;if(eGe(this))return[this];if(this.type==="compound"&&!this.initial)return[this];var t=mUe(this.initialStateValue);return f0(t.map(function(i){return e.getFromRelativePath(i)}))},enumerable:!1,configurable:!0}),n.prototype.getFromRelativePath=function(e){if(!e.length)return[this];var t=xf(e),i=t[0],r=t.slice(1);if(!this.states)throw new Error("Cannot retrieve subPath '".concat(i,"' from node with no states"));var o=this.getStateNode(i);if(o.type==="history")return o.resolveHistory();if(!this.states[i])throw new Error("Child state '".concat(i,"' does not exist on '").concat(this.id,"'"));return this.states[i].getFromRelativePath(r)},n.prototype.historyValue=function(e){if(Object.keys(this.states).length)return{current:e||this.initialStateValue,states:QNn(this.states,function(t,i){if(!e)return t.historyValue();var r=Uf(e)?void 0:e[i];return t.historyValue(r||t.initialStateValue)},function(t){return!t.history})}},n.prototype.resolveHistory=function(e){var t=this;if(this.type!=="history")return[this];var i=this.parent;if(!e){var r=this.target;return r?f0(mUe(r).map(function(l){return i.getFromRelativePath(l)})):i.initialStateNodes}var o=sfr(i.path,"states")(e).current;return Uf(o)?[i.getStateNode(o)]:f0(mUe(o).map(function(l){return t.history==="deep"?i.getFromRelativePath(l):[i.states[l[0]]]}))},Object.defineProperty(n.prototype,"stateIds",{get:function(){var e=this,t=f0(Object.keys(this.states).map(function(i){return e.states[i].stateIds}));return[this.id].concat(t)},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"events",{get:function(){var e,t,i,r;if(this.__cache.events)return this.__cache.events;var o=this.states,l=new Set(this.ownEvents);if(o)try{for(var c=yh(Object.keys(o)),d=c.next();!d.done;d=c.next()){var h=d.value,p=o[h];if(p.states)try{for(var m=(i=void 0,yh(p.events)),b=m.next();!b.done;b=m.next()){var w=b.value;l.add("".concat(w))}}catch(_){i={error:_}}finally{try{b&&!b.done&&(r=m.return)&&r.call(m)}finally{if(i)throw i.error}}}}catch(_){e={error:_}}finally{try{d&&!d.done&&(t=c.return)&&t.call(c)}finally{if(e)throw e.error}}return this.__cache.events=Array.from(l)},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"ownEvents",{get:function(){var e=new Set(this.transitions.filter(function(t){return!(!t.target&&!t.actions.length&&t.internal)}).map(function(t){return t.eventType}));return Array.from(e)},enumerable:!1,configurable:!0}),n.prototype.resolveTarget=function(e){var t=this;if(e!==void 0)return e.map(function(i){if(!Uf(i))return i;var r=i[0]===t.delimiter;if(r&&!t.parent)return t.getStateNodeByPath(i.slice(1));var o=r?t.key+i:i;if(t.parent)try{var l=t.parent.getStateNodeByPath(o);return l}catch(c){throw new Error("Invalid transition definition for state node '".concat(t.id,`': +`).concat(c.message))}else return t.getStateNodeByPath(o)})},n.prototype.formatTransition=function(e){var t=this,i=pfr(e.target),r="internal"in e?e.internal:i?i.some(function(d){return Uf(d)&&d[0]===t.delimiter}):!0,o=this.machine.options.guards,l=this.resolveTarget(i),c=Ws(Ws({},e),{actions:yG(AR(e.actions)),cond:Hci(e.cond,o),target:l,source:this,internal:r,eventType:e.event,toJSON:function(){return Ws(Ws({},c),{target:c.target?c.target.map(function(d){return"#".concat(d.id)}):void 0,source:"#".concat(t.id)})}});return c},n.prototype.formatTransitions=function(){var e,t,i=this,r;if(!this.config.on)r=[];else if(Array.isArray(this.config.on))r=this.config.on;else{var o=this.config.on,l=AEt,c=o[l],d=c===void 0?[]:c,h=Pzt(o,[typeof l=="symbol"?l:l+""]);r=f0(Object.keys(h).map(function(D){var A=nhe(D,h[D]);return A}).concat(nhe(AEt,d)))}var p=this.config.always?nhe("",this.config.always):[],m=this.config.onDone?nhe(String(LBe(this.id)),this.config.onDone):[],b=f0(this.invoke.map(function(D){var A=[];return D.onDone&&A.push.apply(A,$m([],xf(nhe(String(bUe(D.id)),D.onDone)),!1)),D.onError&&A.push.apply(A,$m([],xf(nhe(String(lke(D.id)),D.onError)),!1)),A})),w=this.after,_=f0($m($m($m($m([],xf(m),!1),xf(b),!1),xf(r),!1),xf(p),!1).map(function(D){return AR(D).map(function(A){return i.formatTransition(A)})}));try{for(var x=yh(w),T=x.next();!T.done;T=x.next()){var I=T.value;_.push(I)}}catch(D){e={error:D}}finally{try{T&&!T.done&&(t=x.return)&&t.call(x)}finally{if(e)throw e.error}}return _},n})();function Qg(n,e){return new Yfr(n,e)}var _n=xfr,NY=f5e,Zpe=vfr,Jf=bfr,V3=Tfr,PY=Vci,Yci=kfr,Zfr=Lfr;const u7=L.createContext({setMessage:()=>null});function of({props:n,name:e}){return Dzt({props:n,name:e,defaultTheme:Fet,themeId:v3})}function Xfr({theme:n,...e}){const t=v3 in n?n[v3]:void 0;return k.jsx(_ci,{...e,themeId:t?v3:void 0,theme:t||n})}const ABe={colorSchemeStorageKey:"mui-color-scheme",defaultLightColorScheme:"light",defaultDarkColorScheme:"dark",modeStorageKey:"mui-mode"},{CssVarsProvider:Qfr}=Hdr({themeId:v3,theme:()=>$1e({cssVariables:!0}),colorSchemeStorageKey:ABe.colorSchemeStorageKey,modeStorageKey:ABe.modeStorageKey,defaultColorScheme:{light:ABe.defaultLightColorScheme,dark:ABe.defaultDarkColorScheme},resolveTheme:n=>{const e={...n,typography:kci(n.palette,n.typography)};return e.unstable_sx=function(i){return XK({sx:i,theme:this})},e}}),Jfr=Qfr;function Zci({theme:n,...e}){const t=L.useMemo(()=>{if(typeof n=="function")return n;const i=v3 in n?n[v3]:n;return"colorSchemes"in i?null:"vars"in i?n:{...n,vars:null}},[n]);return t?k.jsx(Xfr,{theme:t,...e}):k.jsx(Jfr,{theme:n,...e})}function $Ot(...n){return n.reduce((e,t)=>t==null?e:function(...r){e.apply(this,r),t.apply(this,r)},()=>{})}const Gs=Adr;function epr(n){return Po("MuiSvgIcon",n)}Fo("MuiSvgIcon",["root","colorPrimary","colorSecondary","colorAction","colorError","colorDisabled","fontSizeInherit","fontSizeSmall","fontSizeMedium","fontSizeLarge"]);const tpr=n=>{const{color:e,fontSize:t,classes:i}=n,r={root:["root",e!=="inherit"&&`color${ri(e)}`,`fontSize${ri(t)}`]};return jo(r,epr,i)},npr=nn("svg",{name:"MuiSvgIcon",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.color!=="inherit"&&e[`color${ri(t.color)}`],e[`fontSize${ri(t.fontSize)}`]]}})(Gs(({theme:n})=>({userSelect:"none",width:"1em",height:"1em",display:"inline-block",flexShrink:0,transition:n.transitions?.create?.("fill",{duration:(n.vars??n).transitions?.duration?.shorter}),variants:[{props:e=>!e.hasSvgAsChild,style:{fill:"currentColor"}},{props:{fontSize:"inherit"},style:{fontSize:"inherit"}},{props:{fontSize:"small"},style:{fontSize:n.typography?.pxToRem?.(20)||"1.25rem"}},{props:{fontSize:"medium"},style:{fontSize:n.typography?.pxToRem?.(24)||"1.5rem"}},{props:{fontSize:"large"},style:{fontSize:n.typography?.pxToRem?.(35)||"2.1875rem"}},...Object.entries((n.vars??n).palette).filter(([,e])=>e&&e.main).map(([e])=>({props:{color:e},style:{color:(n.vars??n).palette?.[e]?.main}})),{props:{color:"action"},style:{color:(n.vars??n).palette?.action?.active}},{props:{color:"disabled"},style:{color:(n.vars??n).palette?.action?.disabled}},{props:{color:"inherit"},style:{color:void 0}}]}))),tGe=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiSvgIcon"}),{children:r,className:o,color:l="inherit",component:c="svg",fontSize:d="medium",htmlColor:h,inheritViewBox:p=!1,titleAccess:m,viewBox:b="0 0 24 24",...w}=i,_=L.isValidElement(r)&&r.type==="svg",x={...i,color:l,component:c,fontSize:d,instanceFontSize:e.fontSize,inheritViewBox:p,viewBox:b,hasSvgAsChild:_},T={};p||(T.viewBox=b);const I=tpr(x);return k.jsxs(npr,{as:c,className:_i(I.root,o),focusable:"false",color:h,"aria-hidden":m?void 0:!0,role:m?"img":void 0,ref:t,...T,...w,..._&&r.props,ownerState:x,children:[_?r.props.children:r,m?k.jsx("title",{children:m}):null]})});tGe.muiName="SvgIcon";function Ya(n,e){function t(i,r){return k.jsx(tGe,{"data-testid":void 0,ref:r,...i,children:n})}return t.muiName=tGe.muiName,L.memo(L.forwardRef(t))}function p5e(n,e=166){let t;function i(...r){const o=()=>{n.apply(this,r)};clearTimeout(t),t=setTimeout(o,e)}return i.clear=()=>{clearTimeout(t)},i}function fv(n){return n&&n.ownerDocument||document}function KL(n){return fv(n).defaultView||window}function zOt(n,e){typeof n=="function"?n(e):n&&(n.current=e)}function E9(n){const{controlled:e,default:t,name:i,state:r="value"}=n,{current:o}=L.useRef(e!==void 0),[l,c]=L.useState(t),d=o?e:l,h=L.useCallback(p=>{o||c(p)},[]);return[d,h]}function db(n){const e=L.useRef(n);return IS(()=>{e.current=n}),L.useRef((...t)=>(0,e.current)(...t)).current}function xm(...n){const e=L.useRef(void 0),t=L.useCallback(i=>{const r=n.map(o=>{if(o==null)return null;if(typeof o=="function"){const l=o,c=l(i);return typeof c=="function"?c:()=>{l(null)}}return o.current=i,()=>{o.current=null}});return()=>{r.forEach(o=>o?.())}},n);return L.useMemo(()=>n.every(i=>i==null)?null:i=>{e.current&&(e.current(),e.current=void 0),i!=null&&(e.current=t(i))},n)}function ipr(n,e){const t=n.charCodeAt(2);return n[0]==="o"&&n[1]==="n"&&t>=65&&t<=90&&typeof e=="function"}function $zt(n,e){if(!n)return e;function t(l,c){const d={};return Object.keys(c).forEach(h=>{ipr(h,c[h])&&typeof l[h]=="function"&&(d[h]=(...p)=>{l[h](...p),c[h](...p)})}),d}if(typeof n=="function"||typeof e=="function")return l=>{const c=typeof e=="function"?e(l):e,d=typeof n=="function"?n({...l,...c}):n,h=_i(l?.className,c?.className,d?.className),p=t(d,c);return{...c,...d,...p,...!!h&&{className:h},...c?.style&&d?.style&&{style:{...c.style,...d.style}},...c?.sx&&d?.sx&&{sx:[...Array.isArray(c.sx)?c.sx:[c.sx],...Array.isArray(d.sx)?d.sx:[d.sx]]}}};const i=e,r=t(n,i),o=_i(i?.className,n?.className);return{...e,...n,...r,...!!o&&{className:o},...i?.style&&n?.style&&{style:{...i.style,...n.style}},...i?.sx&&n?.sx&&{sx:[...Array.isArray(i.sx)?i.sx:[i.sx],...Array.isArray(n.sx)?n.sx:[n.sx]]}}}function dc(n,e){if(n==null)return{};var t={};for(var i in n)if({}.hasOwnProperty.call(n,i)){if(e.indexOf(i)!==-1)continue;t[i]=n[i]}return t}function nGe(n,e){return nGe=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,i){return t.__proto__=i,t},nGe(n,e)}function YW(n,e){n.prototype=Object.create(e.prototype),n.prototype.constructor=n,nGe(n,e)}function rpr(n,e){return n.classList?!!e&&n.classList.contains(e):(" "+(n.className.baseVal||n.className)+" ").indexOf(" "+e+" ")!==-1}function spr(n,e){n.classList?n.classList.add(e):rpr(n,e)||(typeof n.className=="string"?n.className=n.className+" "+e:n.setAttribute("class",(n.className&&n.className.baseVal||"")+" "+e))}function iPn(n,e){return n.replace(new RegExp("(^|\\s)"+e+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function opr(n,e){n.classList?n.classList.remove(e):typeof n.className=="string"?n.className=iPn(n.className,e):n.setAttribute("class",iPn(n.className&&n.className.baseVal||"",e))}var REt={exports:{}},w4={},MEt={exports:{}},OEt={};var rPn;function apr(){return rPn||(rPn=1,(function(n){function e(ne,we){var de=ne.length;ne.push(we);e:for(;0>>1,ye=ne[ce];if(0>>1;cer(me,de))ber(xe,me)?(ne[ce]=xe,ne[be]=de,ce=be):(ne[ce]=me,ne[pe]=de,ce=pe);else if(ber(xe,de))ne[ce]=xe,ne[be]=de,ce=be;else break e}}return we}function r(ne,we){var de=ne.sortIndex-we.sortIndex;return de!==0?de:ne.id-we.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;n.unstable_now=function(){return o.now()}}else{var l=Date,c=l.now();n.unstable_now=function(){return l.now()-c}}var d=[],h=[],p=1,m=null,b=3,w=!1,_=!1,x=!1,T=typeof setTimeout=="function"?setTimeout:null,I=typeof clearTimeout=="function"?clearTimeout:null,D=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function A(ne){for(var we=t(h);we!==null;){if(we.callback===null)i(h);else if(we.startTime<=ne)i(h),we.sortIndex=we.expirationTime,e(d,we);else break;we=t(h)}}function M(ne){if(x=!1,A(ne),!_)if(t(d)!==null)_=!0,se(O);else{var we=t(h);we!==null&&ue(M,we.startTime-ne)}}function O(ne,we){_=!1,x&&(x=!1,I(W),W=-1),w=!0;var de=b;try{for(A(we),m=t(d);m!==null&&(!(m.expirationTime>we)||ne&&!te());){var ce=m.callback;if(typeof ce=="function"){m.callback=null,b=m.priorityLevel;var ye=ce(m.expirationTime<=we);we=n.unstable_now(),typeof ye=="function"?m.callback=ye:m===t(d)&&i(d),A(we)}else i(d);m=t(d)}if(m!==null)var he=!0;else{var pe=t(h);pe!==null&&ue(M,pe.startTime-we),he=!1}return he}finally{m=null,b=de,w=!1}}var F=!1,j=null,W=-1,U=5,Z=-1;function te(){return!(n.unstable_now()-Zne||125ce?(ne.sortIndex=de,e(h,ne),t(d)===null&&ne===t(h)&&(x?(I(W),W=-1):x=!0,ue(M,de-ce))):(ne.sortIndex=ye,e(d,ne),_||w||(_=!0,se(O))),ne},n.unstable_shouldYield=te,n.unstable_wrapCallback=function(ne){var we=b;return function(){var de=b;b=we;try{return ne.apply(this,arguments)}finally{b=de}}}})(OEt)),OEt}var sPn;function lpr(){return sPn||(sPn=1,MEt.exports=apr()),MEt.exports}var oPn;function cpr(){if(oPn)return w4;oPn=1;var n=LM(),e=lpr();function t(P){for(var B="https://reactjs.org/docs/error-decoder.html?invariant="+P,J=1;J"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),d=Object.prototype.hasOwnProperty,h=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,p={},m={};function b(P){return d.call(m,P)?!0:d.call(p,P)?!1:h.test(P)?m[P]=!0:(p[P]=!0,!1)}function w(P,B,J,le){if(J!==null&&J.type===0)return!1;switch(typeof B){case"function":case"symbol":return!0;case"boolean":return le?!1:J!==null?!J.acceptsBooleans:(P=P.toLowerCase().slice(0,5),P!=="data-"&&P!=="aria-");default:return!1}}function _(P,B,J,le){if(B===null||typeof B>"u"||w(P,B,J,le))return!0;if(le)return!1;if(J!==null)switch(J.type){case 3:return!B;case 4:return B===!1;case 5:return isNaN(B);case 6:return isNaN(B)||1>B}return!1}function x(P,B,J,le,ke,je,gt){this.acceptsBooleans=B===2||B===3||B===4,this.attributeName=le,this.attributeNamespace=ke,this.mustUseProperty=J,this.propertyName=P,this.type=B,this.sanitizeURL=je,this.removeEmptyString=gt}var T={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(P){T[P]=new x(P,0,!1,P,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(P){var B=P[0];T[B]=new x(B,1,!1,P[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(P){T[P]=new x(P,2,!1,P.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(P){T[P]=new x(P,2,!1,P,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(P){T[P]=new x(P,3,!1,P.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(P){T[P]=new x(P,3,!0,P,null,!1,!1)}),["capture","download"].forEach(function(P){T[P]=new x(P,4,!1,P,null,!1,!1)}),["cols","rows","size","span"].forEach(function(P){T[P]=new x(P,6,!1,P,null,!1,!1)}),["rowSpan","start"].forEach(function(P){T[P]=new x(P,5,!1,P.toLowerCase(),null,!1,!1)});var I=/[\-:]([a-z])/g;function D(P){return P[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(P){var B=P.replace(I,D);T[B]=new x(B,1,!1,P,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(P){var B=P.replace(I,D);T[B]=new x(B,1,!1,P,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(P){var B=P.replace(I,D);T[B]=new x(B,1,!1,P,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(P){T[P]=new x(P,1,!1,P.toLowerCase(),null,!1,!1)}),T.xlinkHref=new x("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(P){T[P]=new x(P,1,!1,P.toLowerCase(),null,!0,!0)});function A(P,B,J,le){var ke=T.hasOwnProperty(B)?T[B]:null;(ke!==null?ke.type!==0:le||!(2Qt||ke[gt]!==je[Qt]){var vn=` -`+ke[gt].replace(" at new "," at ");return P.displayName&&vn.includes("")&&(vn=vn.replace("",P.displayName)),vn}while(1<=gt&&0<=Qt);break}}}finally{he=!1,Error.prepareStackTrace=J}return(P=P?P.displayName||P.name:"")?ye(P):""}function me(P){switch(P.tag){case 5:return ye(P.type);case 16:return ye("Lazy");case 13:return ye("Suspense");case 19:return ye("SuspenseList");case 0:case 2:case 15:return P=pe(P.type,!1),P;case 11:return P=pe(P.type.render,!1),P;case 1:return P=pe(P.type,!0),P;default:return""}}function be(P){if(P==null)return null;if(typeof P=="function")return P.displayName||P.name||null;if(typeof P=="string")return P;switch(P){case j:return"Fragment";case F:return"Portal";case q:return"Profiler";case W:return"StrictMode";case te:return"Suspense";case Q:return"SuspenseList"}if(typeof P=="object")switch(P.$$typeof){case ee:return(P.displayName||"Context")+".Consumer";case Z:return(P._context.displayName||"Context")+".Provider";case G:var B=P.render;return P=P.displayName,P||(P=B.displayName||B.name||"",P=P!==""?"ForwardRef("+P+")":"ForwardRef"),P;case ie:return B=P.displayName||null,B!==null?B:be(P.type)||"Memo";case se:B=P._payload,P=P._init;try{return be(P(B))}catch{}}return null}function xe(P){var B=P.type;switch(P.tag){case 24:return"Cache";case 9:return(B.displayName||"Context")+".Consumer";case 10:return(B._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return P=B.render,P=P.displayName||P.name||"",B.displayName||(P!==""?"ForwardRef("+P+")":"ForwardRef");case 7:return"Fragment";case 5:return B;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return be(B);case 8:return B===W?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof B=="function")return B.displayName||B.name||null;if(typeof B=="string")return B}return null}function Te(P){switch(typeof P){case"boolean":case"number":case"string":case"undefined":return P;case"object":return P;default:return""}}function Ge(P){var B=P.type;return(P=P.nodeName)&&P.toLowerCase()==="input"&&(B==="checkbox"||B==="radio")}function tt(P){var B=Ge(P)?"checked":"value",J=Object.getOwnPropertyDescriptor(P.constructor.prototype,B),le=""+P[B];if(!P.hasOwnProperty(B)&&typeof J<"u"&&typeof J.get=="function"&&typeof J.set=="function"){var ke=J.get,je=J.set;return Object.defineProperty(P,B,{configurable:!0,get:function(){return ke.call(this)},set:function(gt){le=""+gt,je.call(this,gt)}}),Object.defineProperty(P,B,{enumerable:J.enumerable}),{getValue:function(){return le},setValue:function(gt){le=""+gt},stopTracking:function(){P._valueTracker=null,delete P[B]}}}}function Ue(P){P._valueTracker||(P._valueTracker=tt(P))}function Me(P){if(!P)return!1;var B=P._valueTracker;if(!B)return!0;var J=B.getValue(),le="";return P&&(le=Ge(P)?P.checked?"true":"false":P.value),P=le,P!==J?(B.setValue(P),!0):!1}function He(P){if(P=P||(typeof document<"u"?document:void 0),typeof P>"u")return null;try{return P.activeElement||P.body}catch{return P.body}}function at(P,B){var J=B.checked;return ue({},B,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:J??P._wrapperState.initialChecked})}function rt(P,B){var J=B.defaultValue==null?"":B.defaultValue,le=B.checked!=null?B.checked:B.defaultChecked;J=Te(B.value!=null?B.value:J),P._wrapperState={initialChecked:le,initialValue:J,controlled:B.type==="checkbox"||B.type==="radio"?B.checked!=null:B.value!=null}}function Be(P,B){B=B.checked,B!=null&&A(P,"checked",B,!1)}function lt(P,B){Be(P,B);var J=Te(B.value),le=B.type;if(J!=null)le==="number"?(J===0&&P.value===""||P.value!=J)&&(P.value=""+J):P.value!==""+J&&(P.value=""+J);else if(le==="submit"||le==="reset"){P.removeAttribute("value");return}B.hasOwnProperty("value")?ze(P,B.type,J):B.hasOwnProperty("defaultValue")&&ze(P,B.type,Te(B.defaultValue)),B.checked==null&&B.defaultChecked!=null&&(P.defaultChecked=!!B.defaultChecked)}function ct(P,B,J){if(B.hasOwnProperty("value")||B.hasOwnProperty("defaultValue")){var le=B.type;if(!(le!=="submit"&&le!=="reset"||B.value!==void 0&&B.value!==null))return;B=""+P._wrapperState.initialValue,J||B===P.value||(P.value=B),P.defaultValue=B}J=P.name,J!==""&&(P.name=""),P.defaultChecked=!!P._wrapperState.initialChecked,J!==""&&(P.name=J)}function ze(P,B,J){(B!=="number"||He(P.ownerDocument)!==P)&&(J==null?P.defaultValue=""+P._wrapperState.initialValue:P.defaultValue!==""+J&&(P.defaultValue=""+J))}var Ke=Array.isArray;function $e(P,B,J,le){if(P=P.options,B){B={};for(var ke=0;ke"+B.valueOf().toString()+"",B=zt.firstChild;P.firstChild;)P.removeChild(P.firstChild);for(;B.firstChild;)P.appendChild(B.firstChild)}});function xn(P,B){if(B){var J=P.firstChild;if(J&&J===P.lastChild&&J.nodeType===3){J.nodeValue=B;return}}P.textContent=B}var hn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Zi=["Webkit","ms","Moz","O"];Object.keys(hn).forEach(function(P){Zi.forEach(function(B){B=B+P.charAt(0).toUpperCase()+P.substring(1),hn[B]=hn[P]})});function $i(P,B,J){return B==null||typeof B=="boolean"||B===""?"":J||typeof B!="number"||B===0||hn.hasOwnProperty(P)&&hn[P]?(""+B).trim():B+"px"}function Dr(P,B){P=P.style;for(var J in B)if(B.hasOwnProperty(J)){var le=J.indexOf("--")===0,ke=$i(J,B[J],le);J==="float"&&(J="cssFloat"),le?P.setProperty(J,ke):P[J]=ke}}var ps=ue({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function nn(P,B){if(B){if(ps[P]&&(B.children!=null||B.dangerouslySetInnerHTML!=null))throw Error(t(137,P));if(B.dangerouslySetInnerHTML!=null){if(B.children!=null)throw Error(t(60));if(typeof B.dangerouslySetInnerHTML!="object"||!("__html"in B.dangerouslySetInnerHTML))throw Error(t(61))}if(B.style!=null&&typeof B.style!="object")throw Error(t(62))}}function xt(P,B){if(P.indexOf("-")===-1)return typeof B.is=="string";switch(P){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Ei=null;function gr(P){return P=P.target||P.srcElement||window,P.correspondingUseElement&&(P=P.correspondingUseElement),P.nodeType===3?P.parentNode:P}var ss=null,us=null,_r=null;function uo(P){if(P=MT(P)){if(typeof ss!="function")throw Error(t(280));var B=P.stateNode;B&&(B=OT(B),ss(P.stateNode,P.type,B))}}function xs(P){us?_r?_r.push(P):_r=[P]:us=P}function Fs(){if(us){var P=us,B=_r;if(_r=us=null,uo(P),B)for(P=0;P>>=0,P===0?32:31-(Lp(P)/ud|0)|0}var js=64,Pl=4194304;function hh(P){switch(P&-P){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return P&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return P&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return P}}function uf(P,B){var J=P.pendingLanes;if(J===0)return 0;var le=0,ke=P.suspendedLanes,je=P.pingedLanes,gt=J&268435455;if(gt!==0){var Qt=gt&~ke;Qt!==0?le=hh(Qt):(je&=gt,je!==0&&(le=hh(je)))}else gt=J&~ke,gt!==0?le=hh(gt):je!==0&&(le=hh(je));if(le===0)return 0;if(B!==0&&B!==le&&(B&ke)===0&&(ke=le&-le,je=B&-B,ke>=je||ke===16&&(je&4194240)!==0))return B;if((le&4)!==0&&(le|=J&16),B=P.entangledLanes,B!==0)for(P=P.entanglements,B&=le;0J;J++)B.push(P);return B}function gC(P,B,J){P.pendingLanes|=B,B!==536870912&&(P.suspendedLanes=0,P.pingedLanes=0),P=P.eventTimes,B=31-Io(B),P[B]=J}function HM(P,B){var J=P.pendingLanes&~B;P.pendingLanes=B,P.suspendedLanes=0,P.pingedLanes=0,P.expiredLanes&=B,P.mutableReadLanes&=B,P.entangledLanes&=B,B=P.entanglements;var le=P.eventTimes;for(P=P.expirationTimes;0=Ed),YM=" ",$7=!1;function Sv(P,B){switch(P){case"keyup":return MV.indexOf(B.keyCode)!==-1;case"keydown":return B.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function XE(P){return P=P.detail,typeof P=="object"&&"data"in P?P.data:null}var QE=!1;function JE(P,B){switch(P){case"compositionend":return XE(B);case"keypress":return B.which!==32?null:($7=!0,YM);case"textInput":return P=B.data,P===YM&&$7?null:P;default:return null}}function Zm(P,B){if(QE)return P==="compositionend"||!Dh&&Sv(P,B)?(P=N7(),oI=E8=Z_=null,QE=!1,P):null;switch(P){case"paste":return null;case"keypress":if(!(B.ctrlKey||B.altKey||B.metaKey)||B.ctrlKey&&B.altKey){if(B.char&&1=B)return{node:J,offset:B-P};P=le}e:{for(;J;){if(J.nextSibling){J=J.nextSibling;break e}J=J.parentNode}J=void 0}J=ma(J)}}function x0(P,B){return P&&B?P===B?!0:P&&P.nodeType===3?!1:B&&B.nodeType===3?x0(P,B.parentNode):"contains"in P?P.contains(B):P.compareDocumentPosition?!!(P.compareDocumentPosition(B)&16):!1:!1}function fI(){for(var P=window,B=He();B instanceof P.HTMLIFrameElement;){try{var J=typeof B.contentWindow.location.href=="string"}catch{J=!1}if(J)P=B.contentWindow;else break;B=He(P.document)}return B}function ZM(P){var B=P&&P.nodeName&&P.nodeName.toLowerCase();return B&&(B==="input"&&(P.type==="text"||P.type==="search"||P.type==="tel"||P.type==="url"||P.type==="password")||B==="textarea"||P.contentEditable==="true")}function ik(P){var B=fI(),J=P.focusedElem,le=P.selectionRange;if(B!==J&&J&&J.ownerDocument&&x0(J.ownerDocument.documentElement,J)){if(le!==null&&ZM(J)){if(B=le.start,P=le.end,P===void 0&&(P=B),"selectionStart"in J)J.selectionStart=B,J.selectionEnd=Math.min(P,J.value.length);else if(P=(B=J.ownerDocument||document)&&B.defaultView||window,P.getSelection){P=P.getSelection();var ke=J.textContent.length,je=Math.min(le.start,ke);le=le.end===void 0?je:Math.min(le.end,ke),!P.extend&&je>le&&(ke=le,le=je,je=ke),ke=Bo(J,je);var gt=Bo(J,le);ke&>&&(P.rangeCount!==1||P.anchorNode!==ke.node||P.anchorOffset!==ke.offset||P.focusNode!==gt.node||P.focusOffset!==gt.offset)&&(B=B.createRange(),B.setStart(ke.node,ke.offset),P.removeAllRanges(),je>le?(P.addRange(B),P.extend(gt.node,gt.offset)):(B.setEnd(gt.node,gt.offset),P.addRange(B)))}}for(B=[],P=J;P=P.parentNode;)P.nodeType===1&&B.push({element:P,left:P.scrollLeft,top:P.scrollTop});for(typeof J.focus=="function"&&J.focus(),J=0;J=document.documentMode,LT=null,R8=null,M8=null,XM=!1;function vw(P,B,J){var le=J.window===J?J.document:J.nodeType===9?J:J.ownerDocument;XM||LT==null||LT!==He(le)||(le=LT,"selectionStart"in le&&ZM(le)?le={start:le.selectionStart,end:le.selectionEnd}:(le=(le.ownerDocument&&le.ownerDocument.defaultView||window).getSelection(),le={anchorNode:le.anchorNode,anchorOffset:le.anchorOffset,focusNode:le.focusNode,focusOffset:le.focusOffset}),M8&&wo(M8,le)||(M8=le,le=P8(R8,"onSelect"),0NT||(P.current=iO[NT],iO[NT]=null,NT--)}function Td(P,B){NT++,iO[NT]=P.current,P.current=B}var e2={},E0=ww(e2),yw=ww(!1),j8=e2;function yI(P,B){var J=P.type.contextTypes;if(!J)return e2;var le=P.stateNode;if(le&&le.__reactInternalMemoizedUnmaskedChildContext===B)return le.__reactInternalMemoizedMaskedChildContext;var ke={},je;for(je in J)ke[je]=B[je];return le&&(P=P.stateNode,P.__reactInternalMemoizedUnmaskedChildContext=B,P.__reactInternalMemoizedMaskedChildContext=ke),ke}function Cb(P){return P=P.childContextTypes,P!=null}function _I(){Yd(yw),Yd(E0)}function fZ(P,B,J){if(E0.current!==e2)throw Error(t(168));Td(E0,B),Td(yw,J)}function Y7(P,B,J){var le=P.stateNode;if(B=B.childContextTypes,typeof le.getChildContext!="function")return J;le=le.getChildContext();for(var ke in le)if(!(ke in B))throw Error(t(108,xe(P)||"Unknown",ke));return ue({},J,le)}function rO(P){return P=(P=P.stateNode)&&P.__reactInternalMemoizedMergedChildContext||e2,j8=E0.current,Td(E0,P),Td(yw,yw.current),!0}function PV(P,B,J){var le=P.stateNode;if(!le)throw Error(t(169));J?(P=Y7(P,B,j8),le.__reactInternalMemoizedMergedChildContext=P,Yd(yw),Yd(E0),Td(E0,P)):Yd(yw),Td(yw,J)}var nx=null,H8=!1,FV=!1;function jV(P){nx===null?nx=[P]:nx.push(P)}function pZ(P){H8=!0,jV(P)}function PT(){if(!FV&&nx!==null){FV=!0;var P=0,B=xd;try{var J=nx;for(xd=1;P>=gt,ke-=gt,Fl=1<<32-Io(B)+ke|J<jl?(U1=nl,nl=null):U1=nl.sibling;var Ld=xr(ri,nl,pi[jl],ws);if(Ld===null){nl===null&&(nl=U1);break}P&&nl&&Ld.alternate===null&&B(ri,nl),On=je(Ld,On,jl),Ga===null?ra=Ld:Ga.sibling=Ld,Ga=Ld,nl=U1}if(jl===pi.length)return J(ri,nl),Mf&&FT(ri,jl),ra;if(nl===null){for(;jljl?(U1=nl,nl=null):U1=nl.sibling;var ZT=xr(ri,nl,Ld.value,ws);if(ZT===null){nl===null&&(nl=U1);break}P&&nl&&ZT.alternate===null&&B(ri,nl),On=je(ZT,On,jl),Ga===null?ra=ZT:Ga.sibling=ZT,Ga=ZT,nl=U1}if(Ld.done)return J(ri,nl),Mf&&FT(ri,jl),ra;if(nl===null){for(;!Ld.done;jl++,Ld=pi.next())Ld=pr(ri,Ld.value,ws),Ld!==null&&(On=je(Ld,On,jl),Ga===null?ra=Ld:Ga.sibling=Ld,Ga=Ld);return Mf&&FT(ri,jl),ra}for(nl=le(ri,nl);!Ld.done;jl++,Ld=pi.next())Ld=Ao(nl,ri,jl,Ld.value,ws),Ld!==null&&(P&&Ld.alternate!==null&&nl.delete(Ld.key===null?jl:Ld.key),On=je(Ld,On,jl),Ga===null?ra=Ld:Ga.sibling=Ld,Ga=Ld);return P&&nl.forEach(function(_ae){return B(ri,_ae)}),Mf&&FT(ri,jl),ra}function nm(ri,On,pi,ws){if(typeof pi=="object"&&pi!==null&&pi.type===j&&pi.key===null&&(pi=pi.props.children),typeof pi=="object"&&pi!==null){switch(pi.$$typeof){case O:e:{for(var ra=pi.key,Ga=On;Ga!==null;){if(Ga.key===ra){if(ra=pi.type,ra===j){if(Ga.tag===7){J(ri,Ga.sibling),On=ke(Ga,pi.props.children),On.return=ri,ri=On;break e}}else if(Ga.elementType===ra||typeof ra=="object"&&ra!==null&&ra.$$typeof===se&&uk(ra)===Ga.type){J(ri,Ga.sibling),On=ke(Ga,pi.props),On.ref=V8(ri,Ga,pi),On.return=ri,ri=On;break e}J(ri,Ga);break}else B(ri,Ga);Ga=Ga.sibling}pi.type===j?(On=i5(pi.props.children,ri.mode,ws,pi.key),On.return=ri,ri=On):(ws=TF(pi.type,pi.key,pi.props,null,ri.mode,ws),ws.ref=V8(ri,On,pi),ws.return=ri,ri=ws)}return gt(ri);case F:e:{for(Ga=pi.key;On!==null;){if(On.key===Ga)if(On.tag===4&&On.stateNode.containerInfo===pi.containerInfo&&On.stateNode.implementation===pi.implementation){J(ri,On.sibling),On=ke(On,pi.children||[]),On.return=ri,ri=On;break e}else{J(ri,On);break}else B(ri,On);On=On.sibling}On=L$(pi,ri.mode,ws),On.return=ri,ri=On}return gt(ri);case se:return Ga=pi._init,nm(ri,On,Ga(pi._payload),ws)}if(Ke(pi))return Zo(ri,On,pi,ws);if(we(pi))return Xo(ri,On,pi,ws);SC(ri,pi)}return typeof pi=="string"&&pi!==""||typeof pi=="number"?(pi=""+pi,On!==null&&On.tag===6?(J(ri,On.sibling),On=ke(On,pi),On.return=ri,ri=On):(J(ri,On),On=T$(pi,ri.mode,ws),On.return=ri,ri=On),gt(ri)):J(ri,On)}return nm}var HT=Q7(!0),gZ=Q7(!1),J7=ww(null),eF=null,$8=null,tF=null;function $V(){tF=$8=eF=null}function nF(P){var B=J7.current;Yd(J7),P._currentValue=B}function iF(P,B,J){for(;P!==null;){var le=P.alternate;if((P.childLanes&B)!==B?(P.childLanes|=B,le!==null&&(le.childLanes|=B)):le!==null&&(le.childLanes&B)!==B&&(le.childLanes|=B),P===J)break;P=P.return}}function Qm(P,B){eF=P,tF=$8=null,P=P.dependencies,P!==null&&P.firstContext!==null&&((P.lanes&B)!==0&&(Ew=!0),P.firstContext=null)}function tm(P){var B=P._currentValue;if(tF!==P)if(P={context:P,memoizedValue:B,next:null},$8===null){if(eF===null)throw Error(t(308));$8=P,eF.dependencies={lanes:0,firstContext:P}}else $8=$8.next=P;return B}var z8=null;function zV(P){z8===null?z8=[P]:z8.push(P)}function oO(P,B,J,le){var ke=B.interleaved;return ke===null?(J.next=J,zV(B)):(J.next=ke.next,ke.next=J),B.interleaved=J,xw(P,le)}function xw(P,B){P.lanes|=B;var J=P.alternate;for(J!==null&&(J.lanes|=B),J=P,P=P.return;P!==null;)P.childLanes|=B,J=P.alternate,J!==null&&(J.childLanes|=B),J=P,P=P.return;return J.tag===3?J.stateNode:null}var dk=!1;function rF(P){P.updateQueue={baseState:P.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function mZ(P,B){P=P.updateQueue,B.updateQueue===P&&(B.updateQueue={baseState:P.baseState,firstBaseUpdate:P.firstBaseUpdate,lastBaseUpdate:P.lastBaseUpdate,shared:P.shared,effects:P.effects})}function ix(P,B){return{eventTime:P,lane:B,tag:0,payload:null,callback:null,next:null}}function By(P,B,J){var le=P.updateQueue;if(le===null)return null;if(le=le.shared,(dd&2)!==0){var ke=le.pending;return ke===null?B.next=B:(B.next=ke.next,ke.next=B),le.pending=B,xw(P,J)}return ke=le.interleaved,ke===null?(B.next=B,zV(le)):(B.next=ke.next,ke.next=B),le.interleaved=B,xw(P,J)}function BT(P,B,J){if(B=B.updateQueue,B!==null&&(B=B.shared,(J&4194240)!==0)){var le=B.lanes;le&=P.pendingLanes,J|=le,B.lanes=J,ST(P,J)}}function bZ(P,B){var J=P.updateQueue,le=P.alternate;if(le!==null&&(le=le.updateQueue,J===le)){var ke=null,je=null;if(J=J.firstBaseUpdate,J!==null){do{var gt={eventTime:J.eventTime,lane:J.lane,tag:J.tag,payload:J.payload,callback:J.callback,next:null};je===null?ke=je=gt:je=je.next=gt,J=J.next}while(J!==null);je===null?ke=je=B:je=je.next=B}else ke=je=B;J={baseState:le.baseState,firstBaseUpdate:ke,lastBaseUpdate:je,shared:le.shared,effects:le.effects},P.updateQueue=J;return}P=J.lastBaseUpdate,P===null?J.firstBaseUpdate=B:P.next=B,J.lastBaseUpdate=B}function aO(P,B,J,le){var ke=P.updateQueue;dk=!1;var je=ke.firstBaseUpdate,gt=ke.lastBaseUpdate,Qt=ke.shared.pending;if(Qt!==null){ke.shared.pending=null;var vn=Qt,xi=vn.next;vn.next=null,gt===null?je=xi:gt.next=xi,gt=vn;var Ar=P.alternate;Ar!==null&&(Ar=Ar.updateQueue,Qt=Ar.lastBaseUpdate,Qt!==gt&&(Qt===null?Ar.firstBaseUpdate=xi:Qt.next=xi,Ar.lastBaseUpdate=vn))}if(je!==null){var pr=ke.baseState;gt=0,Ar=xi=vn=null,Qt=je;do{var xr=Qt.lane,Ao=Qt.eventTime;if((le&xr)===xr){Ar!==null&&(Ar=Ar.next={eventTime:Ao,lane:0,tag:Qt.tag,payload:Qt.payload,callback:Qt.callback,next:null});e:{var Zo=P,Xo=Qt;switch(xr=B,Ao=J,Xo.tag){case 1:if(Zo=Xo.payload,typeof Zo=="function"){pr=Zo.call(Ao,pr,xr);break e}pr=Zo;break e;case 3:Zo.flags=Zo.flags&-65537|128;case 0:if(Zo=Xo.payload,xr=typeof Zo=="function"?Zo.call(Ao,pr,xr):Zo,xr==null)break e;pr=ue({},pr,xr);break e;case 2:dk=!0}}Qt.callback!==null&&Qt.lane!==0&&(P.flags|=64,xr=ke.effects,xr===null?ke.effects=[Qt]:xr.push(Qt))}else Ao={eventTime:Ao,lane:xr,tag:Qt.tag,payload:Qt.payload,callback:Qt.callback,next:null},Ar===null?(xi=Ar=Ao,vn=pr):Ar=Ar.next=Ao,gt|=xr;if(Qt=Qt.next,Qt===null){if(Qt=ke.shared.pending,Qt===null)break;xr=Qt,Qt=xr.next,xr.next=null,ke.lastBaseUpdate=xr,ke.shared.pending=null}}while(!0);if(Ar===null&&(vn=pr),ke.baseState=vn,ke.firstBaseUpdate=xi,ke.lastBaseUpdate=Ar,B=ke.shared.interleaved,B!==null){ke=B;do gt|=ke.lane,ke=ke.next;while(ke!==B)}else je===null&&(ke.shared.lanes=0);Q8|=gt,P.lanes=gt,P.memoizedState=pr}}function UV(P,B,J){if(P=B.effects,B.effects=null,P!==null)for(B=0;BJ?J:4,P(!0);var le=GV.transition;GV.transition={};try{P(!1),B()}finally{xd=J,GV.transition=le}}function dF(){return n2().memoizedState}function ax(P,B,J){var le=YT(P);if(J={lane:le,action:J,hasEagerState:!1,eagerState:null,next:null},TZ(P))QV(B,J);else if(J=oO(P,B,J,le),J!==null){var ke=Lv();DC(J,P,le,ke),LZ(J,B,le)}}function Y8(P,B,J){var le=YT(P),ke={lane:le,action:J,hasEagerState:!1,eagerState:null,next:null};if(TZ(P))QV(B,ke);else{var je=P.alternate;if(P.lanes===0&&(je===null||je.lanes===0)&&(je=B.lastRenderedReducer,je!==null))try{var gt=B.lastRenderedState,Qt=je(gt,J);if(ke.hasEagerState=!0,ke.eagerState=Qt,et(Qt,gt)){var vn=B.interleaved;vn===null?(ke.next=ke,zV(B)):(ke.next=vn.next,vn.next=ke),B.interleaved=ke;return}}catch{}J=oO(P,B,ke,le),J!==null&&(ke=Lv(),DC(J,P,le,ke),LZ(J,B,le))}}function TZ(P){var B=P.alternate;return P===sg||B!==null&&B===sg}function QV(P,B){G8=kI=!0;var J=P.pending;J===null?B.next=B:(B.next=J.next,J.next=B),P.pending=B}function LZ(P,B,J){if((J&4194240)!==0){var le=B.lanes;le&=P.pendingLanes,J|=le,B.lanes=J,ST(P,J)}}var hF={readContext:tm,useCallback:zo,useContext:zo,useEffect:zo,useImperativeHandle:zo,useInsertionEffect:zo,useLayoutEffect:zo,useMemo:zo,useReducer:zo,useRef:zo,useState:zo,useDebugValue:zo,useDeferredValue:zo,useTransition:zo,useMutableSource:zo,useSyncExternalStore:zo,useId:zo,unstable_isNewReconciler:!1},sae={readContext:tm,useCallback:function(P,B){return sx().memoizedState=[P,B===void 0?null:B],P},useContext:tm,useEffect:pO,useImperativeHandle:function(P,B,J){return J=J!=null?J.concat([P]):null,UT(4194308,4,xZ.bind(null,B,P),J)},useLayoutEffect:function(P,B){return UT(4194308,4,P,B)},useInsertionEffect:function(P,B){return UT(4,2,P,B)},useMemo:function(P,B){var J=sx();return B=B===void 0?null:B,P=P(),J.memoizedState=[P,B],P},useReducer:function(P,B,J){var le=sx();return B=J!==void 0?J(B):B,le.memoizedState=le.baseState=B,P={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:P,lastRenderedState:B},le.queue=P,P=P.dispatch=ax.bind(null,sg,P),[le.memoizedState,P]},useRef:function(P){var B=sx();return P={current:P},B.memoizedState=P},useState:_Z,useDebugValue:uF,useDeferredValue:function(P){return sx().memoizedState=P},useTransition:function(){var P=_Z(!1),B=P[0];return P=kZ.bind(null,P[1]),sx().memoizedState=P,[B,P]},useMutableSource:function(){},useSyncExternalStore:function(P,B,J){var le=sg,ke=sx();if(Mf){if(J===void 0)throw Error(t(407));J=J()}else{if(J=B(),z1===null)throw Error(t(349));($T&30)!==0||wZ(le,B,J)}ke.memoizedState=J;var je={value:J,getSnapshot:B};return ke.queue=je,pO(ox.bind(null,le,je,P),[P]),le.flags|=2048,K8(9,yZ.bind(null,le,je,J,B),void 0,null),J},useId:function(){var P=sx(),B=z1.identifierPrefix;if(Mf){var J=ck,le=Fl;J=(le&~(1<<32-Io(le)-1)).toString(32)+J,B=":"+B+"R"+J,J=zT++,0Qt||ke[gt]!==je[Qt]){var wn=` +`+ke[gt].replace(" at new "," at ");return P.displayName&&wn.includes("")&&(wn=wn.replace("",P.displayName)),wn}while(1<=gt&&0<=Qt);break}}}finally{he=!1,Error.prepareStackTrace=J}return(P=P?P.displayName||P.name:"")?ye(P):""}function me(P){switch(P.tag){case 5:return ye(P.type);case 16:return ye("Lazy");case 13:return ye("Suspense");case 19:return ye("SuspenseList");case 0:case 2:case 15:return P=pe(P.type,!1),P;case 11:return P=pe(P.type.render,!1),P;case 1:return P=pe(P.type,!0),P;default:return""}}function be(P){if(P==null)return null;if(typeof P=="function")return P.displayName||P.name||null;if(typeof P=="string")return P;switch(P){case j:return"Fragment";case F:return"Portal";case U:return"Profiler";case W:return"StrictMode";case ee:return"Suspense";case Q:return"SuspenseList"}if(typeof P=="object")switch(P.$$typeof){case te:return(P.displayName||"Context")+".Consumer";case Z:return(P._context.displayName||"Context")+".Provider";case G:var B=P.render;return P=P.displayName,P||(P=B.displayName||B.name||"",P=P!==""?"ForwardRef("+P+")":"ForwardRef"),P;case ie:return B=P.displayName||null,B!==null?B:be(P.type)||"Memo";case se:B=P._payload,P=P._init;try{return be(P(B))}catch{}}return null}function xe(P){var B=P.type;switch(P.tag){case 24:return"Cache";case 9:return(B.displayName||"Context")+".Consumer";case 10:return(B._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return P=B.render,P=P.displayName||P.name||"",B.displayName||(P!==""?"ForwardRef("+P+")":"ForwardRef");case 7:return"Fragment";case 5:return B;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return be(B);case 8:return B===W?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof B=="function")return B.displayName||B.name||null;if(typeof B=="string")return B}return null}function Te(P){switch(typeof P){case"boolean":case"number":case"string":case"undefined":return P;case"object":return P;default:return""}}function qe(P){var B=P.type;return(P=P.nodeName)&&P.toLowerCase()==="input"&&(B==="checkbox"||B==="radio")}function et(P){var B=qe(P)?"checked":"value",J=Object.getOwnPropertyDescriptor(P.constructor.prototype,B),le=""+P[B];if(!P.hasOwnProperty(B)&&typeof J<"u"&&typeof J.get=="function"&&typeof J.set=="function"){var ke=J.get,je=J.set;return Object.defineProperty(P,B,{configurable:!0,get:function(){return ke.call(this)},set:function(gt){le=""+gt,je.call(this,gt)}}),Object.defineProperty(P,B,{enumerable:J.enumerable}),{getValue:function(){return le},setValue:function(gt){le=""+gt},stopTracking:function(){P._valueTracker=null,delete P[B]}}}}function Ge(P){P._valueTracker||(P._valueTracker=et(P))}function Me(P){if(!P)return!1;var B=P._valueTracker;if(!B)return!0;var J=B.getValue(),le="";return P&&(le=qe(P)?P.checked?"true":"false":P.value),P=le,P!==J?(B.setValue(P),!0):!1}function He(P){if(P=P||(typeof document<"u"?document:void 0),typeof P>"u")return null;try{return P.activeElement||P.body}catch{return P.body}}function lt(P,B){var J=B.checked;return de({},B,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:J??P._wrapperState.initialChecked})}function st(P,B){var J=B.defaultValue==null?"":B.defaultValue,le=B.checked!=null?B.checked:B.defaultChecked;J=Te(B.value!=null?B.value:J),P._wrapperState={initialChecked:le,initialValue:J,controlled:B.type==="checkbox"||B.type==="radio"?B.checked!=null:B.value!=null}}function Be(P,B){B=B.checked,B!=null&&A(P,"checked",B,!1)}function ot(P,B){Be(P,B);var J=Te(B.value),le=B.type;if(J!=null)le==="number"?(J===0&&P.value===""||P.value!=J)&&(P.value=""+J):P.value!==""+J&&(P.value=""+J);else if(le==="submit"||le==="reset"){P.removeAttribute("value");return}B.hasOwnProperty("value")?ze(P,B.type,J):B.hasOwnProperty("defaultValue")&&ze(P,B.type,Te(B.defaultValue)),B.checked==null&&B.defaultChecked!=null&&(P.defaultChecked=!!B.defaultChecked)}function ct(P,B,J){if(B.hasOwnProperty("value")||B.hasOwnProperty("defaultValue")){var le=B.type;if(!(le!=="submit"&&le!=="reset"||B.value!==void 0&&B.value!==null))return;B=""+P._wrapperState.initialValue,J||B===P.value||(P.value=B),P.defaultValue=B}J=P.name,J!==""&&(P.name=""),P.defaultChecked=!!P._wrapperState.initialChecked,J!==""&&(P.name=J)}function ze(P,B,J){(B!=="number"||He(P.ownerDocument)!==P)&&(J==null?P.defaultValue=""+P._wrapperState.initialValue:P.defaultValue!==""+J&&(P.defaultValue=""+J))}var Ke=Array.isArray;function $e(P,B,J,le){if(P=P.options,B){B={};for(var ke=0;ke"+B.valueOf().toString()+"",B=Ot.firstChild;P.firstChild;)P.removeChild(P.firstChild);for(;B.firstChild;)P.appendChild(B.firstChild)}});function fn(P,B){if(B){var J=P.firstChild;if(J&&J===P.lastChild&&J.nodeType===3){J.nodeValue=B;return}}P.textContent=B}var dn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Gi=["Webkit","ms","Moz","O"];Object.keys(dn).forEach(function(P){Gi.forEach(function(B){B=B+P.charAt(0).toUpperCase()+P.substring(1),dn[B]=dn[P]})});function $i(P,B,J){return B==null||typeof B=="boolean"||B===""?"":J||typeof B!="number"||B===0||dn.hasOwnProperty(P)&&dn[P]?(""+B).trim():B+"px"}function Dr(P,B){P=P.style;for(var J in B)if(B.hasOwnProperty(J)){var le=J.indexOf("--")===0,ke=$i(J,B[J],le);J==="float"&&(J="cssFloat"),le?P.setProperty(J,ke):P[J]=ke}}var ps=de({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function rn(P,B){if(B){if(ps[P]&&(B.children!=null||B.dangerouslySetInnerHTML!=null))throw Error(t(137,P));if(B.dangerouslySetInnerHTML!=null){if(B.children!=null)throw Error(t(60));if(typeof B.dangerouslySetInnerHTML!="object"||!("__html"in B.dangerouslySetInnerHTML))throw Error(t(61))}if(B.style!=null&&typeof B.style!="object")throw Error(t(62))}}function xt(P,B){if(P.indexOf("-")===-1)return typeof B.is=="string";switch(P){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Ei=null;function gr(P){return P=P.target||P.srcElement||window,P.correspondingUseElement&&(P=P.correspondingUseElement),P.nodeType===3?P.parentNode:P}var ss=null,us=null,_r=null;function uo(P){if(P=RT(P)){if(typeof ss!="function")throw Error(t(280));var B=P.stateNode;B&&(B=MT(B),ss(P.stateNode,P.type,B))}}function xs(P){us?_r?_r.push(P):_r=[P]:us=P}function Fs(){if(us){var P=us,B=_r;if(_r=us=null,uo(P),B)for(P=0;P>>=0,P===0?32:31-(Tp(P)/ud|0)|0}var js=64,Fl=4194304;function hh(P){switch(P&-P){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return P&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return P&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return P}}function uf(P,B){var J=P.pendingLanes;if(J===0)return 0;var le=0,ke=P.suspendedLanes,je=P.pingedLanes,gt=J&268435455;if(gt!==0){var Qt=gt&~ke;Qt!==0?le=hh(Qt):(je&=gt,je!==0&&(le=hh(je)))}else gt=J&~ke,gt!==0?le=hh(gt):je!==0&&(le=hh(je));if(le===0)return 0;if(B!==0&&B!==le&&(B&ke)===0&&(ke=le&-le,je=B&-B,ke>=je||ke===16&&(je&4194240)!==0))return B;if((le&4)!==0&&(le|=J&16),B=P.entangledLanes,B!==0)for(P=P.entanglements,B&=le;0J;J++)B.push(P);return B}function gC(P,B,J){P.pendingLanes|=B,B!==536870912&&(P.suspendedLanes=0,P.pingedLanes=0),P=P.eventTimes,B=31-Ao(B),P[B]=J}function WM(P,B){var J=P.pendingLanes&~B;P.pendingLanes=B,P.suspendedLanes=0,P.pingedLanes=0,P.expiredLanes&=B,P.mutableReadLanes&=B,P.entangledLanes&=B,B=P.entanglements;var le=P.eventTimes;for(P=P.expirationTimes;0=Ed),XM=" ",V7=!1;function Sv(P,B){switch(P){case"keyup":return OV.indexOf(B.keyCode)!==-1;case"keydown":return B.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function ZE(P){return P=P.detail,typeof P=="object"&&"data"in P?P.data:null}var XE=!1;function QE(P,B){switch(P){case"compositionend":return ZE(B);case"keypress":return B.which!==32?null:(V7=!0,XM);case"textInput":return P=B.data,P===XM&&V7?null:P;default:return null}}function Zm(P,B){if(XE)return P==="compositionend"||!Dh&&Sv(P,B)?(P=O7(),oI=x8=Q_=null,XE=!1,P):null;switch(P){case"paste":return null;case"keypress":if(!(B.ctrlKey||B.altKey||B.metaKey)||B.ctrlKey&&B.altKey){if(B.char&&1=B)return{node:J,offset:B-P};P=le}e:{for(;J;){if(J.nextSibling){J=J.nextSibling;break e}J=J.parentNode}J=void 0}J=ma(J)}}function E0(P,B){return P&&B?P===B?!0:P&&P.nodeType===3?!1:B&&B.nodeType===3?E0(P,B.parentNode):"contains"in P?P.contains(B):P.compareDocumentPosition?!!(P.compareDocumentPosition(B)&16):!1:!1}function fI(){for(var P=window,B=He();B instanceof P.HTMLIFrameElement;){try{var J=typeof B.contentWindow.location.href=="string"}catch{J=!1}if(J)P=B.contentWindow;else break;B=He(P.document)}return B}function QM(P){var B=P&&P.nodeName&&P.nodeName.toLowerCase();return B&&(B==="input"&&(P.type==="text"||P.type==="search"||P.type==="tel"||P.type==="url"||P.type==="password")||B==="textarea"||P.contentEditable==="true")}function nk(P){var B=fI(),J=P.focusedElem,le=P.selectionRange;if(B!==J&&J&&J.ownerDocument&&E0(J.ownerDocument.documentElement,J)){if(le!==null&&QM(J)){if(B=le.start,P=le.end,P===void 0&&(P=B),"selectionStart"in J)J.selectionStart=B,J.selectionEnd=Math.min(P,J.value.length);else if(P=(B=J.ownerDocument||document)&&B.defaultView||window,P.getSelection){P=P.getSelection();var ke=J.textContent.length,je=Math.min(le.start,ke);le=le.end===void 0?je:Math.min(le.end,ke),!P.extend&&je>le&&(ke=le,le=je,je=ke),ke=Wo(J,je);var gt=Wo(J,le);ke&>&&(P.rangeCount!==1||P.anchorNode!==ke.node||P.anchorOffset!==ke.offset||P.focusNode!==gt.node||P.focusOffset!==gt.offset)&&(B=B.createRange(),B.setStart(ke.node,ke.offset),P.removeAllRanges(),je>le?(P.addRange(B),P.extend(gt.node,gt.offset)):(B.setEnd(gt.node,gt.offset),P.addRange(B)))}}for(B=[],P=J;P=P.parentNode;)P.nodeType===1&&B.push({element:P,left:P.scrollLeft,top:P.scrollTop});for(typeof J.focus=="function"&&J.focus(),J=0;J=document.documentMode,TT=null,A8=null,R8=null,JM=!1;function ww(P,B,J){var le=J.window===J?J.document:J.nodeType===9?J:J.ownerDocument;JM||TT==null||TT!==He(le)||(le=TT,"selectionStart"in le&&QM(le)?le={start:le.selectionStart,end:le.selectionEnd}:(le=(le.ownerDocument&&le.ownerDocument.defaultView||window).getSelection(),le={anchorNode:le.anchorNode,anchorOffset:le.anchorOffset,focusNode:le.focusNode,focusOffset:le.focusOffset}),R8&&wo(R8,le)||(R8=le,le=N8(A8,"onSelect"),0OT||(P.current=sO[OT],sO[OT]=null,OT--)}function Td(P,B){OT++,sO[OT]=P.current,P.current=B}var n2={},k0=yw(n2),_w=yw(!1),F8=n2;function yI(P,B){var J=P.type.contextTypes;if(!J)return n2;var le=P.stateNode;if(le&&le.__reactInternalMemoizedUnmaskedChildContext===B)return le.__reactInternalMemoizedMaskedChildContext;var ke={},je;for(je in J)ke[je]=B[je];return le&&(P=P.stateNode,P.__reactInternalMemoizedUnmaskedChildContext=B,P.__reactInternalMemoizedMaskedChildContext=ke),ke}function Sb(P){return P=P.childContextTypes,P!=null}function _I(){Yd(_w),Yd(k0)}function hZ(P,B,J){if(k0.current!==n2)throw Error(t(168));Td(k0,B),Td(_w,J)}function K7(P,B,J){var le=P.stateNode;if(B=B.childContextTypes,typeof le.getChildContext!="function")return J;le=le.getChildContext();for(var ke in le)if(!(ke in B))throw Error(t(108,xe(P)||"Unknown",ke));return de({},J,le)}function oO(P){return P=(P=P.stateNode)&&P.__reactInternalMemoizedMergedChildContext||n2,F8=k0.current,Td(k0,P),Td(_w,_w.current),!0}function FV(P,B,J){var le=P.stateNode;if(!le)throw Error(t(169));J?(P=K7(P,B,F8),le.__reactInternalMemoizedMergedChildContext=P,Yd(_w),Yd(k0),Td(k0,P)):Yd(_w),Td(_w,J)}var nx=null,j8=!1,jV=!1;function HV(P){nx===null?nx=[P]:nx.push(P)}function fZ(P){j8=!0,HV(P)}function NT(){if(!jV&&nx!==null){jV=!0;var P=0,B=xd;try{var J=nx;for(xd=1;P>=gt,ke-=gt,jl=1<<32-Ao(B)+ke|J<Hl?(q1=nl,nl=null):q1=nl.sibling;var Ld=xr(si,nl,pi[Hl],ws);if(Ld===null){nl===null&&(nl=q1);break}P&&nl&&Ld.alternate===null&&B(si,nl),On=je(Ld,On,Hl),Ga===null?sa=Ld:Ga.sibling=Ld,Ga=Ld,nl=q1}if(Hl===pi.length)return J(si,nl),Rf&&PT(si,Hl),sa;if(nl===null){for(;HlHl?(q1=nl,nl=null):q1=nl.sibling;var YT=xr(si,nl,Ld.value,ws);if(YT===null){nl===null&&(nl=q1);break}P&&nl&&YT.alternate===null&&B(si,nl),On=je(YT,On,Hl),Ga===null?sa=YT:Ga.sibling=YT,Ga=YT,nl=q1}if(Ld.done)return J(si,nl),Rf&&PT(si,Hl),sa;if(nl===null){for(;!Ld.done;Hl++,Ld=pi.next())Ld=pr(si,Ld.value,ws),Ld!==null&&(On=je(Ld,On,Hl),Ga===null?sa=Ld:Ga.sibling=Ld,Ga=Ld);return Rf&&PT(si,Hl),sa}for(nl=le(si,nl);!Ld.done;Hl++,Ld=pi.next())Ld=Ro(nl,si,Hl,Ld.value,ws),Ld!==null&&(P&&Ld.alternate!==null&&nl.delete(Ld.key===null?Hl:Ld.key),On=je(Ld,On,Hl),Ga===null?sa=Ld:Ga.sibling=Ld,Ga=Ld);return P&&nl.forEach(function(vae){return B(si,vae)}),Rf&&PT(si,Hl),sa}function nm(si,On,pi,ws){if(typeof pi=="object"&&pi!==null&&pi.type===j&&pi.key===null&&(pi=pi.props.children),typeof pi=="object"&&pi!==null){switch(pi.$$typeof){case O:e:{for(var sa=pi.key,Ga=On;Ga!==null;){if(Ga.key===sa){if(sa=pi.type,sa===j){if(Ga.tag===7){J(si,Ga.sibling),On=ke(Ga,pi.props.children),On.return=si,si=On;break e}}else if(Ga.elementType===sa||typeof sa=="object"&&sa!==null&&sa.$$typeof===se&&ck(sa)===Ga.type){J(si,Ga.sibling),On=ke(Ga,pi.props),On.ref=W8(si,Ga,pi),On.return=si,si=On;break e}J(si,Ga);break}else B(si,Ga);Ga=Ga.sibling}pi.type===j?(On=n5(pi.props.children,si.mode,ws,pi.key),On.return=si,si=On):(ws=kF(pi.type,pi.key,pi.props,null,si.mode,ws),ws.ref=W8(si,On,pi),ws.return=si,si=ws)}return gt(si);case F:e:{for(Ga=pi.key;On!==null;){if(On.key===Ga)if(On.tag===4&&On.stateNode.containerInfo===pi.containerInfo&&On.stateNode.implementation===pi.implementation){J(si,On.sibling),On=ke(On,pi.children||[]),On.return=si,si=On;break e}else{J(si,On);break}else B(si,On);On=On.sibling}On=D$(pi,si.mode,ws),On.return=si,si=On}return gt(si);case se:return Ga=pi._init,nm(si,On,Ga(pi._payload),ws)}if(Ke(pi))return Xo(si,On,pi,ws);if(we(pi))return Qo(si,On,pi,ws);SC(si,pi)}return typeof pi=="string"&&pi!==""||typeof pi=="number"?(pi=""+pi,On!==null&&On.tag===6?(J(si,On.sibling),On=ke(On,pi),On.return=si,si=On):(J(si,On),On=L$(pi,si.mode,ws),On.return=si,si=On),gt(si)):J(si,On)}return nm}var jT=X7(!0),pZ=X7(!1),Q7=yw(null),J7=null,V8=null,eF=null;function zV(){eF=V8=J7=null}function tF(P){var B=Q7.current;Yd(Q7),P._currentValue=B}function nF(P,B,J){for(;P!==null;){var le=P.alternate;if((P.childLanes&B)!==B?(P.childLanes|=B,le!==null&&(le.childLanes|=B)):le!==null&&(le.childLanes&B)!==B&&(le.childLanes|=B),P===J)break;P=P.return}}function Qm(P,B){J7=P,eF=V8=null,P=P.dependencies,P!==null&&P.firstContext!==null&&((P.lanes&B)!==0&&(kw=!0),P.firstContext=null)}function tm(P){var B=P._currentValue;if(eF!==P)if(P={context:P,memoizedValue:B,next:null},V8===null){if(J7===null)throw Error(t(308));V8=P,J7.dependencies={lanes:0,firstContext:P}}else V8=V8.next=P;return B}var $8=null;function UV(P){$8===null?$8=[P]:$8.push(P)}function lO(P,B,J,le){var ke=B.interleaved;return ke===null?(J.next=J,UV(B)):(J.next=ke.next,ke.next=J),B.interleaved=J,Ew(P,le)}function Ew(P,B){P.lanes|=B;var J=P.alternate;for(J!==null&&(J.lanes|=B),J=P,P=P.return;P!==null;)P.childLanes|=B,J=P.alternate,J!==null&&(J.childLanes|=B),J=P,P=P.return;return J.tag===3?J.stateNode:null}var uk=!1;function iF(P){P.updateQueue={baseState:P.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function gZ(P,B){P=P.updateQueue,B.updateQueue===P&&(B.updateQueue={baseState:P.baseState,firstBaseUpdate:P.firstBaseUpdate,lastBaseUpdate:P.lastBaseUpdate,shared:P.shared,effects:P.effects})}function ix(P,B){return{eventTime:P,lane:B,tag:0,payload:null,callback:null,next:null}}function $y(P,B,J){var le=P.updateQueue;if(le===null)return null;if(le=le.shared,(dd&2)!==0){var ke=le.pending;return ke===null?B.next=B:(B.next=ke.next,ke.next=B),le.pending=B,Ew(P,J)}return ke=le.interleaved,ke===null?(B.next=B,UV(le)):(B.next=ke.next,ke.next=B),le.interleaved=B,Ew(P,J)}function HT(P,B,J){if(B=B.updateQueue,B!==null&&(B=B.shared,(J&4194240)!==0)){var le=B.lanes;le&=P.pendingLanes,J|=le,B.lanes=J,CT(P,J)}}function mZ(P,B){var J=P.updateQueue,le=P.alternate;if(le!==null&&(le=le.updateQueue,J===le)){var ke=null,je=null;if(J=J.firstBaseUpdate,J!==null){do{var gt={eventTime:J.eventTime,lane:J.lane,tag:J.tag,payload:J.payload,callback:J.callback,next:null};je===null?ke=je=gt:je=je.next=gt,J=J.next}while(J!==null);je===null?ke=je=B:je=je.next=B}else ke=je=B;J={baseState:le.baseState,firstBaseUpdate:ke,lastBaseUpdate:je,shared:le.shared,effects:le.effects},P.updateQueue=J;return}P=J.lastBaseUpdate,P===null?J.firstBaseUpdate=B:P.next=B,J.lastBaseUpdate=B}function cO(P,B,J,le){var ke=P.updateQueue;uk=!1;var je=ke.firstBaseUpdate,gt=ke.lastBaseUpdate,Qt=ke.shared.pending;if(Qt!==null){ke.shared.pending=null;var wn=Qt,xi=wn.next;wn.next=null,gt===null?je=xi:gt.next=xi,gt=wn;var Ar=P.alternate;Ar!==null&&(Ar=Ar.updateQueue,Qt=Ar.lastBaseUpdate,Qt!==gt&&(Qt===null?Ar.firstBaseUpdate=xi:Qt.next=xi,Ar.lastBaseUpdate=wn))}if(je!==null){var pr=ke.baseState;gt=0,Ar=xi=wn=null,Qt=je;do{var xr=Qt.lane,Ro=Qt.eventTime;if((le&xr)===xr){Ar!==null&&(Ar=Ar.next={eventTime:Ro,lane:0,tag:Qt.tag,payload:Qt.payload,callback:Qt.callback,next:null});e:{var Xo=P,Qo=Qt;switch(xr=B,Ro=J,Qo.tag){case 1:if(Xo=Qo.payload,typeof Xo=="function"){pr=Xo.call(Ro,pr,xr);break e}pr=Xo;break e;case 3:Xo.flags=Xo.flags&-65537|128;case 0:if(Xo=Qo.payload,xr=typeof Xo=="function"?Xo.call(Ro,pr,xr):Xo,xr==null)break e;pr=de({},pr,xr);break e;case 2:uk=!0}}Qt.callback!==null&&Qt.lane!==0&&(P.flags|=64,xr=ke.effects,xr===null?ke.effects=[Qt]:xr.push(Qt))}else Ro={eventTime:Ro,lane:xr,tag:Qt.tag,payload:Qt.payload,callback:Qt.callback,next:null},Ar===null?(xi=Ar=Ro,wn=pr):Ar=Ar.next=Ro,gt|=xr;if(Qt=Qt.next,Qt===null){if(Qt=ke.shared.pending,Qt===null)break;xr=Qt,Qt=xr.next,xr.next=null,ke.lastBaseUpdate=xr,ke.shared.pending=null}}while(!0);if(Ar===null&&(wn=pr),ke.baseState=wn,ke.firstBaseUpdate=xi,ke.lastBaseUpdate=Ar,B=ke.shared.interleaved,B!==null){ke=B;do gt|=ke.lane,ke=ke.next;while(ke!==B)}else je===null&&(ke.shared.lanes=0);X8|=gt,P.lanes=gt,P.memoizedState=pr}}function qV(P,B,J){if(P=B.effects,B.effects=null,P!==null)for(B=0;BJ?J:4,P(!0);var le=KV.transition;KV.transition={};try{P(!1),B()}finally{xd=J,KV.transition=le}}function uF(){return r2().memoizedState}function ax(P,B,J){var le=KT(P);if(J={lane:le,action:J,hasEagerState:!1,eagerState:null,next:null},kZ(P))JV(B,J);else if(J=lO(P,B,J,le),J!==null){var ke=Lv();DC(J,P,le,ke),TZ(J,B,le)}}function K8(P,B,J){var le=KT(P),ke={lane:le,action:J,hasEagerState:!1,eagerState:null,next:null};if(kZ(P))JV(B,ke);else{var je=P.alternate;if(P.lanes===0&&(je===null||je.lanes===0)&&(je=B.lastRenderedReducer,je!==null))try{var gt=B.lastRenderedState,Qt=je(gt,J);if(ke.hasEagerState=!0,ke.eagerState=Qt,Je(Qt,gt)){var wn=B.interleaved;wn===null?(ke.next=ke,UV(B)):(ke.next=wn.next,wn.next=ke),B.interleaved=ke;return}}catch{}J=lO(P,B,ke,le),J!==null&&(ke=Lv(),DC(J,P,le,ke),TZ(J,B,le))}}function kZ(P){var B=P.alternate;return P===sg||B!==null&&B===sg}function JV(P,B){q8=kI=!0;var J=P.pending;J===null?B.next=B:(B.next=J.next,J.next=B),P.pending=B}function TZ(P,B,J){if((J&4194240)!==0){var le=B.lanes;le&=P.pendingLanes,J|=le,B.lanes=J,CT(P,J)}}var dF={readContext:tm,useCallback:Uo,useContext:Uo,useEffect:Uo,useImperativeHandle:Uo,useInsertionEffect:Uo,useLayoutEffect:Uo,useMemo:Uo,useReducer:Uo,useRef:Uo,useState:Uo,useDebugValue:Uo,useDeferredValue:Uo,useTransition:Uo,useMutableSource:Uo,useSyncExternalStore:Uo,useId:Uo,unstable_isNewReconciler:!1},nae={readContext:tm,useCallback:function(P,B){return sx().memoizedState=[P,B===void 0?null:B],P},useContext:tm,useEffect:mO,useImperativeHandle:function(P,B,J){return J=J!=null?J.concat([P]):null,zT(4194308,4,SZ.bind(null,B,P),J)},useLayoutEffect:function(P,B){return zT(4194308,4,P,B)},useInsertionEffect:function(P,B){return zT(4,2,P,B)},useMemo:function(P,B){var J=sx();return B=B===void 0?null:B,P=P(),J.memoizedState=[P,B],P},useReducer:function(P,B,J){var le=sx();return B=J!==void 0?J(B):B,le.memoizedState=le.baseState=B,P={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:P,lastRenderedState:B},le.queue=P,P=P.dispatch=ax.bind(null,sg,P),[le.memoizedState,P]},useRef:function(P){var B=sx();return P={current:P},B.memoizedState=P},useState:yZ,useDebugValue:cF,useDeferredValue:function(P){return sx().memoizedState=P},useTransition:function(){var P=yZ(!1),B=P[0];return P=EZ.bind(null,P[1]),sx().memoizedState=P,[B,P]},useMutableSource:function(){},useSyncExternalStore:function(P,B,J){var le=sg,ke=sx();if(Rf){if(J===void 0)throw Error(t(407));J=J()}else{if(J=B(),U1===null)throw Error(t(349));(VT&30)!==0||vZ(le,B,J)}ke.memoizedState=J;var je={value:J,getSnapshot:B};return ke.queue=je,mO(ox.bind(null,le,je,P),[P]),le.flags|=2048,G8(9,wZ.bind(null,le,je,J,B),void 0,null),J},useId:function(){var P=sx(),B=U1.identifierPrefix;if(Rf){var J=lk,le=jl;J=(le&~(1<<32-Ao(le)-1)).toString(32)+J,B=":"+B+"R"+J,J=$T++,0<\/script>",P=P.removeChild(P.firstChild)):typeof le.is=="string"?P=gt.createElement(J,{is:le.is}):(P=gt.createElement(J),J==="select"&&(gt=P,le.multiple?gt.multiple=!0:le.size&&(gt.size=le.size))):P=gt.createElementNS(P,J),P[zc]=B,P[jo]=le,bO(P,B,!1,!1),B.stateNode=P;e:{switch(gt=xt(J,le),J){case"dialog":Kh("cancel",P),Kh("close",P),ke=le;break;case"iframe":case"object":case"embed":Kh("load",P),ke=le;break;case"video":case"audio":for(ke=0;keFI&&(B.flags|=128,le=!0,qT(je,!1),B.lanes=4194304)}else{if(!le)if(P=cO(gt),P!==null){if(B.flags|=128,le=!0,J=P.updateQueue,J!==null&&(B.updateQueue=J,B.flags|=4),qT(je,!0),je.tail===null&&je.tailMode==="hidden"&&!gt.alternate&&!Mf)return Eb(B),null}else 2*Es()-je.renderingStartTime>FI&&J!==1073741824&&(B.flags|=128,le=!0,qT(je,!1),B.lanes=4194304);je.isBackwards?(gt.sibling=B.child,B.child=gt):(J=je.last,J!==null?J.sibling=gt:B.child=gt,je.last=gt)}return je.tail!==null?(B=je.tail,je.rendering=B,je.tail=B.sibling,je.renderingStartTime=Es(),B.sibling=null,J=sp.current,Td(sp,le?J&1|2:J&1),B):(Eb(B),null);case 22:case 23:return EF(),le=B.memoizedState!==null,P!==null&&P.memoizedState!==null!==le&&(B.flags|=8192),le&&(B.mode&1)!==0?(Vy&1073741824)!==0&&(Eb(B),B.subtreeFlags&6&&(B.flags|=8192)):Eb(B),null;case 24:return null;case 25:return null}throw Error(t(156,B.tag))}function cae(P,B){switch(Cw(B),B.tag){case 1:return Cb(B.type)&&_I(),P=B.flags,P&65536?(B.flags=P&-65537|128,B):null;case 3:return q8(),Yd(yw),Yd(E0),oF(),P=B.flags,(P&65536)!==0&&(P&128)===0?(B.flags=P&-65537|128,B):null;case 5:return sF(B),null;case 13:if(Yd(sp),P=B.memoizedState,P!==null&&P.dehydrated!==null){if(B.alternate===null)throw Error(t(340));jT()}return P=B.flags,P&65536?(B.flags=P&-65537|128,B):null;case 19:return Yd(sp),null;case 4:return q8(),null;case 10:return nF(B.type._context),null;case 22:case 23:return EF(),null;case 24:return null;default:return null}}var vF=!1,kb=!1,uae=typeof WeakSet=="function"?WeakSet:Set,Ho=null;function RI(P,B){var J=P.ref;if(J!==null)if(typeof J=="function")try{J(null)}catch(le){Og(P,B,le)}else J.current=null}function d$(P,B,J){try{J()}catch(le){Og(P,B,le)}}var UZ=!1;function wF(P,B){if(Ve=bC,P=fI(),ZM(P)){if("selectionStart"in P)var J={start:P.selectionStart,end:P.selectionEnd};else e:{J=(J=P.ownerDocument)&&J.defaultView||window;var le=J.getSelection&&J.getSelection();if(le&&le.rangeCount!==0){J=le.anchorNode;var ke=le.anchorOffset,je=le.focusNode;le=le.focusOffset;try{J.nodeType,je.nodeType}catch{J=null;break e}var gt=0,Qt=-1,vn=-1,xi=0,Ar=0,pr=P,xr=null;t:for(;;){for(var Ao;pr!==J||ke!==0&&pr.nodeType!==3||(Qt=gt+ke),pr!==je||le!==0&&pr.nodeType!==3||(vn=gt+le),pr.nodeType===3&&(gt+=pr.nodeValue.length),(Ao=pr.firstChild)!==null;)xr=pr,pr=Ao;for(;;){if(pr===P)break t;if(xr===J&&++xi===ke&&(Qt=gt),xr===je&&++Ar===le&&(vn=gt),(Ao=pr.nextSibling)!==null)break;pr=xr,xr=pr.parentNode}pr=Ao}J=Qt===-1||vn===-1?null:{start:Qt,end:vn}}else J=null}J=J||{start:0,end:0}}else J=null;for(ae={focusedElem:P,selectionRange:J},bC=!1,Ho=B;Ho!==null;)if(B=Ho,P=B.child,(B.subtreeFlags&1028)!==0&&P!==null)P.return=B,Ho=P;else for(;Ho!==null;){B=Ho;try{var Zo=B.alternate;if((B.flags&1024)!==0)switch(B.tag){case 0:case 11:case 15:break;case 1:if(Zo!==null){var Xo=Zo.memoizedProps,nm=Zo.memoizedState,ri=B.stateNode,On=ri.getSnapshotBeforeUpdate(B.elementType===B.type?Xo:$1(B.type,Xo),nm);ri.__reactInternalSnapshotBeforeUpdate=On}break;case 3:var pi=B.stateNode.containerInfo;pi.nodeType===1?pi.textContent="":pi.nodeType===9&&pi.documentElement&&pi.removeChild(pi.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(t(163))}}catch(ws){Og(B,B.return,ws)}if(P=B.sibling,P!==null){P.return=B.return,Ho=P;break}Ho=B.return}return Zo=UZ,UZ=!1,Zo}function MI(P,B,J){var le=B.updateQueue;if(le=le!==null?le.lastEffect:null,le!==null){var ke=le=le.next;do{if((ke.tag&P)===P){var je=ke.destroy;ke.destroy=void 0,je!==void 0&&d$(B,J,je)}ke=ke.next}while(ke!==le)}}function OI(P,B){if(B=B.updateQueue,B=B!==null?B.lastEffect:null,B!==null){var J=B=B.next;do{if((J.tag&P)===P){var le=J.create;J.destroy=le()}J=J.next}while(J!==B)}}function h$(P){var B=P.ref;if(B!==null){var J=P.stateNode;P.tag,P=J,typeof B=="function"?B(P):B.current=P}}function X8(P){var B=P.alternate;B!==null&&(P.alternate=null,X8(B)),P.child=null,P.deletions=null,P.sibling=null,P.tag===5&&(B=P.stateNode,B!==null&&(delete B[zc],delete B[jo],delete B[Ev],delete B[Q_],delete B[RT])),P.stateNode=null,P.return=null,P.dependencies=null,P.memoizedProps=null,P.memoizedState=null,P.pendingProps=null,P.stateNode=null,P.updateQueue=null}function f$(P){return P.tag===5||P.tag===3||P.tag===4}function qZ(P){e:for(;;){for(;P.sibling===null;){if(P.return===null||f$(P.return))return null;P=P.return}for(P.sibling.return=P.return,P=P.sibling;P.tag!==5&&P.tag!==6&&P.tag!==18;){if(P.flags&2||P.child===null||P.tag===4)continue e;P.child.return=P,P=P.child}if(!(P.flags&2))return P.stateNode}}function p$(P,B,J){var le=P.tag;if(le===5||le===6)P=P.stateNode,B?J.nodeType===8?J.parentNode.insertBefore(P,B):J.insertBefore(P,B):(J.nodeType===8?(B=J.parentNode,B.insertBefore(P,J)):(B=J,B.appendChild(P)),J=J._reactRootContainer,J!=null||B.onclick!==null||(B.onclick=wI));else if(le!==4&&(P=P.child,P!==null))for(p$(P,B,J),P=P.sibling;P!==null;)p$(P,B,J),P=P.sibling}function g$(P,B,J){var le=P.tag;if(le===5||le===6)P=P.stateNode,B?J.insertBefore(P,B):J.appendChild(P);else if(le!==4&&(P=P.child,P!==null))for(g$(P,B,J),P=P.sibling;P!==null;)g$(P,B,J),P=P.sibling}var k0=null,cx=!1;function GT(P,B,J){for(J=J.child;J!==null;)GZ(P,B,J),J=J.sibling}function GZ(P,B,J){if(cd&&typeof cd.onCommitFiberUnmount=="function")try{cd.onCommitFiberUnmount(Pu,J)}catch{}switch(J.tag){case 5:kb||RI(J,B);case 6:var le=k0,ke=cx;k0=null,GT(P,B,J),k0=le,cx=ke,k0!==null&&(cx?(P=k0,J=J.stateNode,P.nodeType===8?P.parentNode.removeChild(J):P.removeChild(J)):k0.removeChild(J.stateNode));break;case 18:k0!==null&&(cx?(P=k0,J=J.stateNode,P.nodeType===8?Qn(P.parentNode,J):P.nodeType===1&&Qn(P,J),Mg(P)):Qn(k0,J.stateNode));break;case 4:le=k0,ke=cx,k0=J.stateNode.containerInfo,cx=!0,GT(P,B,J),k0=le,cx=ke;break;case 0:case 11:case 14:case 15:if(!kb&&(le=J.updateQueue,le!==null&&(le=le.lastEffect,le!==null))){ke=le=le.next;do{var je=ke,gt=je.destroy;je=je.tag,gt!==void 0&&((je&2)!==0||(je&4)!==0)&&d$(J,B,gt),ke=ke.next}while(ke!==le)}GT(P,B,J);break;case 1:if(!kb&&(RI(J,B),le=J.stateNode,typeof le.componentWillUnmount=="function"))try{le.props=J.memoizedProps,le.state=J.memoizedState,le.componentWillUnmount()}catch(Qt){Og(J,B,Qt)}GT(P,B,J);break;case 21:GT(P,B,J);break;case 22:J.mode&1?(kb=(le=kb)||J.memoizedState!==null,GT(P,B,J),kb=le):GT(P,B,J);break;default:GT(P,B,J)}}function KZ(P){var B=P.updateQueue;if(B!==null){P.updateQueue=null;var J=P.stateNode;J===null&&(J=P.stateNode=new uae),B.forEach(function(le){var ke=pae.bind(null,P,le);J.has(le)||(J.add(le),le.then(ke,ke))})}}function kC(P,B){var J=B.deletions;if(J!==null)for(var le=0;leke&&(ke=gt),le&=~je}if(le=ke,le=Es()-le,le=(120>le?120:480>le?480:1080>le?1080:1920>le?1920:3e3>le?3e3:4320>le?4320:1960*NI(le/1960))-le,10P?16:P,pk===null)var le=!1;else{if(P=pk,pk=null,J8=0,(dd&6)!==0)throw Error(t(331));var ke=dd;for(dd|=4,Ho=P.current;Ho!==null;){var je=Ho,gt=je.child;if((Ho.flags&16)!==0){var Qt=je.deletions;if(Qt!==null){for(var vn=0;vnEs()-y$?Dv(P,0):Tb|=J),Lw(P,B)}function tX(P,B){B===0&&((P.mode&1)===0?B=1:(B=Pl,Pl<<=1,(Pl&130023424)===0&&(Pl=4194304)));var J=Lv();P=xw(P,B),P!==null&&(gC(P,B,J),Lw(P,J))}function E$(P){var B=P.memoizedState,J=0;B!==null&&(J=B.retryLane),tX(P,J)}function pae(P,B){var J=0;switch(P.tag){case 13:var le=P.stateNode,ke=P.memoizedState;ke!==null&&(J=ke.retryLane);break;case 19:le=P.stateNode;break;default:throw Error(t(314))}le!==null&&le.delete(B),tX(P,J)}var kF;kF=function(P,B,J){if(P!==null)if(P.memoizedProps!==B.pendingProps||yw.current)Ew=!0;else{if((P.lanes&J)===0&&(B.flags&128)===0)return Ew=!1,$Z(P,B,J);Ew=(P.flags&131072)!==0}else Ew=!1,Mf&&(B.flags&1048576)!==0&&xI(B,sO,B.index);switch(B.lanes=0,B.tag){case 2:var le=B.type;II(P,B),P=B.pendingProps;var ke=yI(B,E0.current);Qm(B,J),ke=dO(null,B,le,P,ke,J);var je=hk();return B.flags|=1,typeof ke=="object"&&ke!==null&&typeof ke.render=="function"&&ke.$$typeof===void 0?(B.tag=1,B.memoizedState=null,B.updateQueue=null,Cb(le)?(je=!0,rO(B)):je=!1,B.memoizedState=ke.state!==null&&ke.state!==void 0?ke.state:null,rF(B),ke.updater=pF,B.stateNode=ke,ke._reactInternals=B,n$(B,le,P,J),B=s$(null,B,le,!0,je,J)):(B.tag=0,Mf&&je&&HV(B),Tv(null,B,ke,J),B=B.child),B;case 16:le=B.elementType;e:{switch(II(P,B),P=B.pendingProps,ke=le._init,le=ke(le._payload),B.type=le,ke=B.tag=gae(le),P=$1(le,P),ke){case 0:B=r$(null,B,le,P,J);break e;case 1:B=FZ(null,B,le,P,J);break e;case 11:B=OZ(null,B,le,P,J);break e;case 14:B=mF(null,B,le,$1(le.type,P),J);break e}throw Error(t(306,le,""))}return B;case 0:return le=B.type,ke=B.pendingProps,ke=B.elementType===le?ke:$1(le,ke),r$(P,B,le,ke,J);case 1:return le=B.type,ke=B.pendingProps,ke=B.elementType===le?ke:$1(le,ke),FZ(P,B,le,ke,J);case 3:e:{if(jZ(B),P===null)throw Error(t(387));le=B.pendingProps,je=B.memoizedState,ke=je.element,mZ(P,B),aO(B,le,null,J);var gt=B.memoizedState;if(le=gt.element,je.isDehydrated)if(je={element:le,isDehydrated:!1,cache:gt.cache,pendingSuspenseBoundaries:gt.pendingSuspenseBoundaries,transitions:gt.transitions},B.updateQueue.baseState=je,B.memoizedState=je,B.flags&256){ke=DI(Error(t(423)),B),B=HZ(P,B,le,J,ke);break e}else if(le!==ke){ke=DI(Error(t(424)),B),B=HZ(P,B,le,J,ke);break e}else for(Sb=Tr(B.stateNode.containerInfo.firstChild),jy=B,Mf=!0,Hy=null,J=gZ(B,null,le,J),B.child=J;J;)J.flags=J.flags&-3|4096,J=J.sibling;else{if(jT(),le===ke){B=lx(P,B,J);break e}Tv(P,B,le,J)}B=B.child}return B;case 5:return lO(B),P===null&&EI(B),le=B.type,ke=B.pendingProps,je=P!==null?P.memoizedProps:null,gt=ke.children,Oe(le,ke)?gt=null:je!==null&&Oe(le,je)&&(B.flags|=32),kn(P,B),Tv(P,B,gt,J),B.child;case 6:return P===null&&EI(B),null;case 13:return BZ(P,B,J);case 4:return qV(B,B.stateNode.containerInfo),le=B.pendingProps,P===null?B.child=HT(B,null,le,J):Tv(P,B,le,J),B.child;case 11:return le=B.type,ke=B.pendingProps,ke=B.elementType===le?ke:$1(le,ke),OZ(P,B,le,ke,J);case 7:return Tv(P,B,B.pendingProps,J),B.child;case 8:return Tv(P,B,B.pendingProps.children,J),B.child;case 12:return Tv(P,B,B.pendingProps.children,J),B.child;case 10:e:{if(le=B.type._context,ke=B.pendingProps,je=B.memoizedProps,gt=ke.value,Td(J7,le._currentValue),le._currentValue=gt,je!==null)if(et(je.value,gt)){if(je.children===ke.children&&!yw.current){B=lx(P,B,J);break e}}else for(je=B.child,je!==null&&(je.return=B);je!==null;){var Qt=je.dependencies;if(Qt!==null){gt=je.child;for(var vn=Qt.firstContext;vn!==null;){if(vn.context===le){if(je.tag===1){vn=ix(-1,J&-J),vn.tag=2;var xi=je.updateQueue;if(xi!==null){xi=xi.shared;var Ar=xi.pending;Ar===null?vn.next=vn:(vn.next=Ar.next,Ar.next=vn),xi.pending=vn}}je.lanes|=J,vn=je.alternate,vn!==null&&(vn.lanes|=J),iF(je.return,J,B),Qt.lanes|=J;break}vn=vn.next}}else if(je.tag===10)gt=je.type===B.type?null:je.child;else if(je.tag===18){if(gt=je.return,gt===null)throw Error(t(341));gt.lanes|=J,Qt=gt.alternate,Qt!==null&&(Qt.lanes|=J),iF(gt,J,B),gt=je.sibling}else gt=je.child;if(gt!==null)gt.return=je;else for(gt=je;gt!==null;){if(gt===B){gt=null;break}if(je=gt.sibling,je!==null){je.return=gt.return,gt=je;break}gt=gt.return}je=gt}Tv(P,B,ke.children,J),B=B.child}return B;case 9:return ke=B.type,le=B.pendingProps.children,Qm(B,J),ke=tm(ke),le=le(ke),B.flags|=1,Tv(P,B,le,J),B.child;case 14:return le=B.type,ke=$1(le,B.pendingProps),ke=$1(le.type,ke),mF(P,B,le,ke,J);case 15:return NZ(P,B,B.type,B.pendingProps,J);case 17:return le=B.type,ke=B.pendingProps,ke=B.elementType===le?ke:$1(le,ke),II(P,B),B.tag=1,Cb(le)?(P=!0,rO(B)):P=!1,Qm(B,J),e$(B,le,ke),n$(B,le,ke,J),s$(null,B,le,!0,P,J);case 19:return VZ(P,B,J);case 22:return PZ(P,B,J)}throw Error(t(156,B.tag))};function t1(P,B){return _n(P,B)}function Iw(P,B,J,le){this.tag=P,this.key=J,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=B,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=le,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function o2(P,B,J,le){return new Iw(P,B,J,le)}function k$(P){return P=P.prototype,!(!P||!P.isReactComponent)}function gae(P){if(typeof P=="function")return k$(P)?1:0;if(P!=null){if(P=P.$$typeof,P===G)return 11;if(P===ie)return 14}return 2}function gk(P,B){var J=P.alternate;return J===null?(J=o2(P.tag,B,P.key,P.mode),J.elementType=P.elementType,J.type=P.type,J.stateNode=P.stateNode,J.alternate=P,P.alternate=J):(J.pendingProps=B,J.type=P.type,J.flags=0,J.subtreeFlags=0,J.deletions=null),J.flags=P.flags&14680064,J.childLanes=P.childLanes,J.lanes=P.lanes,J.child=P.child,J.memoizedProps=P.memoizedProps,J.memoizedState=P.memoizedState,J.updateQueue=P.updateQueue,B=P.dependencies,J.dependencies=B===null?null:{lanes:B.lanes,firstContext:B.firstContext},J.sibling=P.sibling,J.index=P.index,J.ref=P.ref,J}function TF(P,B,J,le,ke,je){var gt=2;if(le=P,typeof P=="function")k$(P)&&(gt=1);else if(typeof P=="string")gt=5;else e:switch(P){case j:return i5(J.children,ke,je,B);case W:gt=8,ke|=8;break;case q:return P=o2(12,J,B,ke|2),P.elementType=q,P.lanes=je,P;case te:return P=o2(13,J,B,ke),P.elementType=te,P.lanes=je,P;case Q:return P=o2(19,J,B,ke),P.elementType=Q,P.lanes=je,P;case de:return LF(J,ke,je,B);default:if(typeof P=="object"&&P!==null)switch(P.$$typeof){case Z:gt=10;break e;case ee:gt=9;break e;case G:gt=11;break e;case ie:gt=14;break e;case se:gt=16,le=null;break e}throw Error(t(130,P==null?P:typeof P,""))}return B=o2(gt,J,B,ke),B.elementType=P,B.type=le,B.lanes=je,B}function i5(P,B,J,le){return P=o2(7,P,le,B),P.lanes=J,P}function LF(P,B,J,le){return P=o2(22,P,le,B),P.elementType=de,P.lanes=J,P.stateNode={isHidden:!1},P}function T$(P,B,J){return P=o2(6,P,null,B),P.lanes=J,P}function L$(P,B,J){return B=o2(4,P.children!==null?P.children:[],P.key,B),B.lanes=J,B.stateNode={containerInfo:P.containerInfo,pendingChildren:null,implementation:P.implementation},B}function mae(P,B,J,le,ke){this.tag=B,this.containerInfo=P,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=KE(0),this.expirationTimes=KE(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=KE(0),this.identifierPrefix=le,this.onRecoverableError=ke,this.mutableSourceEagerHydrationData=null}function D$(P,B,J,le,ke,je,gt,Qt,vn){return P=new mae(P,B,J,Qt,vn),B===1?(B=1,je===!0&&(B|=8)):B=0,je=o2(3,null,null,B),P.current=je,je.stateNode=P,je.memoizedState={element:le,isDehydrated:J,cache:null,transitions:null,pendingSuspenseBoundaries:null},rF(je),P}function HI(P,B,J){var le=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}return n(),REt.exports=ipr(),REt.exports}var KR=qzt();const Ffe=Vs(KR),aPn={disabled:!1},iGe=Rt.createContext(null);var Kci=function(e){return e.scrollTop},fke="unmounted",Gte="exited",Kte="entering",gfe="entered",qOt="exiting",l8=(function(n){ZW(e,n);function e(i,r){var o;o=n.call(this,i,r)||this;var l=r,c=l&&!l.isMounting?i.enter:i.appear,d;return o.appearStatus=null,i.in?c?(d=Gte,o.appearStatus=Kte):d=gfe:i.unmountOnExit||i.mountOnEnter?d=fke:d=Gte,o.state={status:d},o.nextCallback=null,o}e.getDerivedStateFromProps=function(r,o){var l=r.in;return l&&o.status===fke?{status:Gte}:null};var t=e.prototype;return t.componentDidMount=function(){this.updateStatus(!0,this.appearStatus)},t.componentDidUpdate=function(r){var o=null;if(r!==this.props){var l=this.state.status;this.props.in?l!==Kte&&l!==gfe&&(o=Kte):(l===Kte||l===gfe)&&(o=qOt)}this.updateStatus(!1,o)},t.componentWillUnmount=function(){this.cancelNextCallback()},t.getTimeouts=function(){var r=this.props.timeout,o,l,c;return o=l=c=r,r!=null&&typeof r!="number"&&(o=r.exit,l=r.enter,c=r.appear!==void 0?r.appear:l),{exit:o,enter:l,appear:c}},t.updateStatus=function(r,o){if(r===void 0&&(r=!1),o!==null)if(this.cancelNextCallback(),o===Kte){if(this.props.unmountOnExit||this.props.mountOnEnter){var l=this.props.nodeRef?this.props.nodeRef.current:Ffe.findDOMNode(this);l&&Kci(l)}this.performEnter(r)}else this.performExit();else this.props.unmountOnExit&&this.state.status===Gte&&this.setState({status:fke})},t.performEnter=function(r){var o=this,l=this.props.enter,c=this.context?this.context.isMounting:r,d=this.props.nodeRef?[c]:[Ffe.findDOMNode(this),c],h=d[0],p=d[1],m=this.getTimeouts(),b=c?m.appear:m.enter;if(!r&&!l||aPn.disabled){this.safeSetState({status:gfe},function(){o.props.onEntered(h)});return}this.props.onEnter(h,p),this.safeSetState({status:Kte},function(){o.props.onEntering(h,p),o.onTransitionEnd(b,function(){o.safeSetState({status:gfe},function(){o.props.onEntered(h,p)})})})},t.performExit=function(){var r=this,o=this.props.exit,l=this.getTimeouts(),c=this.props.nodeRef?void 0:Ffe.findDOMNode(this);if(!o||aPn.disabled){this.safeSetState({status:Gte},function(){r.props.onExited(c)});return}this.props.onExit(c),this.safeSetState({status:qOt},function(){r.props.onExiting(c),r.onTransitionEnd(l.exit,function(){r.safeSetState({status:Gte},function(){r.props.onExited(c)})})})},t.cancelNextCallback=function(){this.nextCallback!==null&&(this.nextCallback.cancel(),this.nextCallback=null)},t.safeSetState=function(r,o){o=this.setNextCallback(o),this.setState(r,o)},t.setNextCallback=function(r){var o=this,l=!0;return this.nextCallback=function(c){l&&(l=!1,o.nextCallback=null,r(c))},this.nextCallback.cancel=function(){l=!1},this.nextCallback},t.onTransitionEnd=function(r,o){this.setNextCallback(o);var l=this.props.nodeRef?this.props.nodeRef.current:Ffe.findDOMNode(this),c=r==null&&!this.props.addEndListener;if(!l||c){setTimeout(this.nextCallback,0);return}if(this.props.addEndListener){var d=this.props.nodeRef?[this.nextCallback]:[l,this.nextCallback],h=d[0],p=d[1];this.props.addEndListener(h,p)}r!=null&&setTimeout(this.nextCallback,r)},t.render=function(){var r=this.state.status;if(r===fke)return null;var o=this.props,l=o.children;o.in,o.mountOnEnter,o.unmountOnExit,o.appear,o.enter,o.exit,o.timeout,o.addEndListener,o.onEnter,o.onEntering,o.onEntered,o.onExit,o.onExiting,o.onExited,o.nodeRef;var c=uc(o,["children","in","mountOnEnter","unmountOnExit","appear","enter","exit","timeout","addEndListener","onEnter","onEntering","onEntered","onExit","onExiting","onExited","nodeRef"]);return Rt.createElement(iGe.Provider,{value:null},typeof l=="function"?l(r,c):Rt.cloneElement(Rt.Children.only(l),c))},e})(Rt.Component);l8.contextType=iGe;l8.propTypes={};function lhe(){}l8.defaultProps={in:!1,mountOnEnter:!1,unmountOnExit:!1,appear:!1,enter:!0,exit:!0,onEnter:lhe,onEntering:lhe,onEntered:lhe,onExit:lhe,onExiting:lhe,onExited:lhe};l8.UNMOUNTED=fke;l8.EXITED=Gte;l8.ENTERING=Kte;l8.ENTERED=gfe;l8.EXITING=qOt;var rpr=function(e,t){return e&&t&&t.split(" ").forEach(function(i){return Jfr(e,i)})},NEt=function(e,t){return e&&t&&t.split(" ").forEach(function(i){return epr(e,i)})},Gzt=(function(n){ZW(e,n);function e(){for(var i,r=arguments.length,o=new Array(r),l=0;l{this.currentId=null,t()},e)}clear=()=>{this.currentId!==null&&(clearTimeout(this.currentId),this.currentId=null)};disposeEffect=()=>this.clear}function NG(){const n=Yci(Wet.create).current;return dpr(n.disposeEffect),n}const Yzt=n=>n.scrollTop;function QK(n,e){const{timeout:t,easing:i,style:r={}}=n;return{duration:r.transitionDuration??(typeof t=="number"?t:t[e.mode]||0),easing:r.transitionTimingFunction??(typeof i=="object"?i[e.mode]:i),delay:r.transitionDelay}}function x9(n){return typeof n=="string"}function Zci(n,e,t){return n===void 0||x9(n)?e:{...e,ownerState:{...e.ownerState,...t}}}function Xci(n,e,t){return typeof n=="function"?n(e,t):n}function vie(n,e=[]){if(n===void 0)return{};const t={};return Object.keys(n).filter(i=>i.match(/^on[A-Z]/)&&typeof n[i]=="function"&&!e.includes(i)).forEach(i=>{t[i]=n[i]}),t}function cPn(n){if(n===void 0)return{};const e={};return Object.keys(n).filter(t=>!(t.match(/^on[A-Z]/)&&typeof n[t]=="function")).forEach(t=>{e[t]=n[t]}),e}function Qci(n){const{getSlotProps:e,additionalProps:t,externalSlotProps:i,externalForwardedProps:r,className:o}=n;if(!e){const w=_i(t?.className,o,r?.className,i?.className),_={...t?.style,...r?.style,...i?.style},x={...t,...r,...i};return w.length>0&&(x.className=w),Object.keys(_).length>0&&(x.style=_),{props:x,internalRef:void 0}}const l=vie({...r,...i}),c=cPn(i),d=cPn(r),h=e(l),p=_i(h?.className,t?.className,o,r?.className,i?.className),m={...h?.style,...t?.style,...r?.style,...i?.style},b={...h,...t,...d,...c};return p.length>0&&(b.className=p),Object.keys(m).length>0&&(b.style=m),{props:b,internalRef:h.ref}}function _o(n,e){const{className:t,elementType:i,ownerState:r,externalForwardedProps:o,internalForwardedProps:l,shouldForwardComponentProp:c=!1,...d}=e,{component:h,slots:p={[n]:void 0},slotProps:m={[n]:void 0},...b}=o,w=p[n]||i,_=Xci(m[n],r),{props:{component:x,...T},internalRef:I}=Qci({className:t,...d,externalForwardedProps:n==="root"?b:void 0,externalSlotProps:_}),L=xm(I,_?.ref,e.ref),A=n==="root"?x||h:x,M=Zci(w,{...n==="root"&&!h&&!p[n]&&l,...n!=="root"&&!p[n]&&l,...T,...A&&!c&&{as:A},...A&&c&&{component:A},ref:L},r);return[w,M]}function hpr(n){return No("MuiCollapse",n)}Po("MuiCollapse",["root","horizontal","vertical","entered","hidden","wrapper","wrapperInner"]);const fpr=n=>{const{orientation:e,classes:t}=n;return Fo({root:["root",e],entered:["entered"],hidden:["hidden"],wrapper:["wrapper",e],wrapperInner:["wrapperInner",e]},hpr,t)},ppr=tn("div",{name:"MuiCollapse",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.orientation],t.state==="entered"&&e.entered,t.state==="exited"&&!t.in&&t.collapsedSize==="0px"&&e.hidden]}})(Gs(({theme:n})=>({height:0,overflow:"hidden",transition:n.transitions.create("height"),variants:[{props:{orientation:"horizontal"},style:{height:"auto",width:0,transition:n.transitions.create("width")}},{props:{state:"entered"},style:{height:"auto",overflow:"visible"}},{props:{state:"entered",orientation:"horizontal"},style:{width:"auto"}},{props:({ownerState:e})=>e.state==="exited"&&!e.in&&e.collapsedSize==="0px",style:{visibility:"hidden"}}]}))),gpr=tn("div",{name:"MuiCollapse",slot:"Wrapper"})({display:"flex",width:"100%",variants:[{props:{orientation:"horizontal"},style:{width:"auto",height:"100%"}}]}),mpr=tn("div",{name:"MuiCollapse",slot:"WrapperInner"})({width:"100%",variants:[{props:{orientation:"horizontal"},style:{width:"auto",height:"100%"}}]}),fLe=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiCollapse"}),{addEndListener:r,children:o,className:l,collapsedSize:c="0px",component:d,easing:h,in:p,onEnter:m,onEntered:b,onEntering:w,onExit:_,onExited:x,onExiting:T,orientation:I="vertical",slots:L={},slotProps:A={},style:M,timeout:O=xci.standard,TransitionComponent:F=l8,...j}=i,W={...i,orientation:I,collapsedSize:c},q=fpr(W),Z=Lf(),ee=NG(),G=D.useRef(null),te=D.useRef(),Q=typeof c=="number"?`${c}px`:c,ie=I==="horizontal",se=ie?"width":"height",de=D.useRef(null),ne=xm(t,de),we=rt=>Be=>{if(rt){const lt=de.current;Be===void 0?rt(lt):rt(lt,Be)}},ue=()=>G.current?G.current[ie?"clientWidth":"clientHeight"]:0,ce=we((rt,Be)=>{G.current&&ie&&(G.current.style.position="absolute"),rt.style[se]=Q,m&&m(rt,Be)}),ye=we((rt,Be)=>{const lt=ue();G.current&&ie&&(G.current.style.position="");const{duration:ct,easing:ze}=QK({style:M,timeout:O,easing:h},{mode:"enter"});if(O==="auto"){const Ke=Z.transitions.getAutoHeightDuration(lt);rt.style.transitionDuration=`${Ke}ms`,te.current=Ke}else rt.style.transitionDuration=typeof ct=="string"?ct:`${ct}ms`;rt.style[se]=`${lt}px`,rt.style.transitionTimingFunction=ze,w&&w(rt,Be)}),he=we((rt,Be)=>{rt.style[se]="auto",b&&b(rt,Be)}),pe=we(rt=>{rt.style[se]=`${ue()}px`,_&&_(rt)}),me=we(x),be=we(rt=>{const Be=ue(),{duration:lt,easing:ct}=QK({style:M,timeout:O,easing:h},{mode:"exit"});if(O==="auto"){const ze=Z.transitions.getAutoHeightDuration(Be);rt.style.transitionDuration=`${ze}ms`,te.current=ze}else rt.style.transitionDuration=typeof lt=="string"?lt:`${lt}ms`;rt.style[se]=Q,rt.style.transitionTimingFunction=ct,T&&T(rt)}),xe=rt=>{O==="auto"&&ee.start(te.current||0,rt),r&&r(de.current,rt)},Te={slots:L,slotProps:A,component:d},[Ge,tt]=_o("root",{ref:ne,className:_i(q.root,l),elementType:ppr,externalForwardedProps:Te,ownerState:W,additionalProps:{style:{[ie?"minWidth":"minHeight"]:Q,...M}}}),[Ue,Me]=_o("wrapper",{ref:G,className:q.wrapper,elementType:gpr,externalForwardedProps:Te,ownerState:W}),[He,at]=_o("wrapperInner",{className:q.wrapperInner,elementType:mpr,externalForwardedProps:Te,ownerState:W});return k.jsx(F,{in:p,onEnter:ce,onEntered:he,onEntering:ye,onExit:pe,onExited:me,onExiting:be,addEndListener:xe,nodeRef:de,timeout:O==="auto"?null:O,...j,children:(rt,{ownerState:Be,...lt})=>{const ct={...W,state:rt};return k.jsx(Ge,{...tt,className:_i(tt.className,{entered:q.entered,exited:!p&&Q==="0px"&&q.hidden}[rt]),ownerState:ct,...lt,children:k.jsx(Ue,{...Me,ownerState:ct,children:k.jsx(He,{...at,ownerState:ct,children:o})})})}})});fLe&&(fLe.muiSupportAuto=!0);function bpr(n){return No("MuiPaper",n)}Po("MuiPaper",["root","rounded","outlined","elevation","elevation0","elevation1","elevation2","elevation3","elevation4","elevation5","elevation6","elevation7","elevation8","elevation9","elevation10","elevation11","elevation12","elevation13","elevation14","elevation15","elevation16","elevation17","elevation18","elevation19","elevation20","elevation21","elevation22","elevation23","elevation24"]);const vpr=n=>{const{square:e,elevation:t,variant:i,classes:r}=n,o={root:["root",i,!e&&"rounded",i==="elevation"&&`elevation${t}`]};return Fo(o,bpr,r)},wpr=tn("div",{name:"MuiPaper",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.variant],!t.square&&e.rounded,t.variant==="elevation"&&e[`elevation${t.elevation}`]]}})(Gs(({theme:n})=>({backgroundColor:(n.vars||n).palette.background.paper,color:(n.vars||n).palette.text.primary,transition:n.transitions.create("box-shadow"),variants:[{props:({ownerState:e})=>!e.square,style:{borderRadius:n.shape.borderRadius}},{props:{variant:"outlined"},style:{border:`1px solid ${(n.vars||n).palette.divider}`}},{props:{variant:"elevation"},style:{boxShadow:"var(--Paper-shadow)",backgroundImage:"var(--Paper-overlay)"}}]}))),Jf=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiPaper"}),r=Lf(),{className:o,component:l="div",elevation:c=1,square:d=!1,variant:h="elevation",...p}=i,m={...i,component:l,elevation:c,square:d,variant:h},b=vpr(m);return k.jsx(wpr,{as:l,ownerState:m,className:_i(b.root,o),ref:t,...p,style:{...h==="elevation"&&{"--Paper-shadow":(r.vars||r).shadows[c],...r.vars&&{"--Paper-overlay":r.vars.overlays?.[c]},...!r.vars&&r.palette.mode==="dark"&&{"--Paper-overlay":`linear-gradient(${Wa("#fff",MOt(c))}, ${Wa("#fff",MOt(c))})`}},...p.style}})}),Jci=D.createContext({});function ypr(n){return No("MuiAccordion",n)}const RBe=Po("MuiAccordion",["root","heading","rounded","expanded","disabled","gutters","region"]),_pr=n=>{const{classes:e,square:t,expanded:i,disabled:r,disableGutters:o}=n;return Fo({root:["root",!t&&"rounded",i&&"expanded",r&&"disabled",!o&&"gutters"],heading:["heading"],region:["region"]},ypr,e)},Cpr=tn(Jf,{name:"MuiAccordion",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[{[`& .${RBe.region}`]:e.region},e.root,!t.square&&e.rounded,!t.disableGutters&&e.gutters]}})(Gs(({theme:n})=>{const e={duration:n.transitions.duration.shortest};return{position:"relative",transition:n.transitions.create(["margin"],e),overflowAnchor:"none","&::before":{position:"absolute",left:0,top:-1,right:0,height:1,content:'""',opacity:1,backgroundColor:(n.vars||n).palette.divider,transition:n.transitions.create(["opacity","background-color"],e)},"&:first-of-type":{"&::before":{display:"none"}},[`&.${RBe.expanded}`]:{"&::before":{opacity:0},"&:first-of-type":{marginTop:0},"&:last-of-type":{marginBottom:0},"& + &":{"&::before":{display:"none"}}},[`&.${RBe.disabled}`]:{backgroundColor:(n.vars||n).palette.action.disabledBackground}}}),Gs(({theme:n})=>({variants:[{props:e=>!e.square,style:{borderRadius:0,"&:first-of-type":{borderTopLeftRadius:(n.vars||n).shape.borderRadius,borderTopRightRadius:(n.vars||n).shape.borderRadius},"&:last-of-type":{borderBottomLeftRadius:(n.vars||n).shape.borderRadius,borderBottomRightRadius:(n.vars||n).shape.borderRadius,"@supports (-ms-ime-align: auto)":{borderBottomLeftRadius:0,borderBottomRightRadius:0}}}},{props:e=>!e.disableGutters,style:{[`&.${RBe.expanded}`]:{margin:"16px 0"}}}]}))),Spr=tn("h3",{name:"MuiAccordion",slot:"Heading"})({all:"unset"}),xpr=tn("div",{name:"MuiAccordion",slot:"Region"})({}),Zzt=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiAccordion"}),{children:r,className:o,defaultExpanded:l=!1,disabled:c=!1,disableGutters:d=!1,expanded:h,onChange:p,slots:m={},slotProps:b={},TransitionComponent:w,TransitionProps:_,...x}=i,[T,I]=S9({controlled:h,default:l,name:"Accordion",state:"expanded"}),L=D.useCallback(we=>{I(!T),p&&p(we,!T)},[T,p,I]),[A,...M]=D.Children.toArray(r),O=D.useMemo(()=>({expanded:T,disabled:c,disableGutters:d,toggle:L}),[T,c,d,L]),F={...i,disabled:c,disableGutters:d,expanded:T},j=_pr(F),W={transition:w,...m},q={transition:_,...b},Z={slots:W,slotProps:q},[ee,G]=_o("root",{elementType:Cpr,externalForwardedProps:{...Z,...x},className:_i(j.root,o),shouldForwardComponentProp:!0,ownerState:F,ref:t}),[te,Q]=_o("heading",{elementType:Spr,externalForwardedProps:Z,className:j.heading,ownerState:F}),[ie,se]=_o("transition",{elementType:fLe,externalForwardedProps:Z,ownerState:F}),[de,ne]=_o("region",{elementType:xpr,externalForwardedProps:Z,ownerState:F,className:j.region,additionalProps:{"aria-labelledby":A.props.id,id:A.props["aria-controls"],role:"region"}});return k.jsxs(ee,{...G,children:[k.jsx(te,{...Q,children:k.jsx(Jci.Provider,{value:O,children:A})}),k.jsx(ie,{in:T,timeout:"auto",...se,children:k.jsx(de,{...ne,children:M})})]})});function Epr(n){return No("MuiAccordionDetails",n)}Po("MuiAccordionDetails",["root"]);const kpr=n=>{const{classes:e}=n;return Fo({root:["root"]},Epr,e)},Tpr=tn("div",{name:"MuiAccordionDetails",slot:"Root"})(Gs(({theme:n})=>({padding:n.spacing(1,2,2)}))),Xzt=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiAccordionDetails"}),{className:r,...o}=i,l=i,c=kpr(l);return k.jsx(Tpr,{className:_i(c.root,r),ref:t,ownerState:l,...o})});function JK(n){try{return n.matches(":focus-visible")}catch{}return!1}class rGe{static create(){return new rGe}static use(){const e=Yci(rGe.create).current,[t,i]=D.useState(!1);return e.shouldMount=t,e.setShouldMount=i,D.useEffect(e.mountEffect,[t]),e}constructor(){this.ref={current:null},this.mounted=null,this.didMount=!1,this.shouldMount=!1,this.setShouldMount=null}mount(){return this.mounted||(this.mounted=Dpr(),this.shouldMount=!0,this.setShouldMount(this.shouldMount)),this.mounted}mountEffect=()=>{this.shouldMount&&!this.didMount&&this.ref.current!==null&&(this.didMount=!0,this.mounted.resolve())};start(...e){this.mount().then(()=>this.ref.current?.start(...e))}stop(...e){this.mount().then(()=>this.ref.current?.stop(...e))}pulsate(...e){this.mount().then(()=>this.ref.current?.pulsate(...e))}}function Lpr(){return rGe.use()}function Dpr(){let n,e;const t=new Promise((i,r)=>{n=i,e=r});return t.resolve=n,t.reject=e,t}function Ipr(n){const{className:e,classes:t,pulsate:i=!1,rippleX:r,rippleY:o,rippleSize:l,in:c,onExited:d,timeout:h}=n,[p,m]=D.useState(!1),b=_i(e,t.ripple,t.rippleVisible,i&&t.ripplePulsate),w={width:l,height:l,top:-(l/2)+o,left:-(l/2)+r},_=_i(t.child,p&&t.childLeaving,i&&t.childPulsate);return!c&&!p&&m(!0),D.useEffect(()=>{if(!c&&d!=null){const x=setTimeout(d,h);return()=>{clearTimeout(x)}}},[d,c,h]),k.jsx("span",{className:b,style:w,children:k.jsx("span",{className:_})})}const QD=Po("MuiTouchRipple",["root","ripple","rippleVisible","ripplePulsate","child","childLeaving","childPulsate"]),GOt=550,Apr=80,Rpr=W3` +`+je.stack}return{value:P,source:B,stack:ke,digest:null}}function r$(P,B,J){return{value:P,source:null,stack:J??null,digest:B??null}}function pF(P,B){try{console.error(B.value)}catch(J){setTimeout(function(){throw J})}}var rae=typeof WeakMap=="function"?WeakMap:Map;function LZ(P,B,J){J=ix(-1,J),J.tag=3,J.payload={element:null};var le=B.value;return J.callback=function(){yF||(yF=!0,_O=le),pF(P,B)},J}function DZ(P,B,J){J=ix(-1,J),J.tag=3;var le=P.type.getDerivedStateFromError;if(typeof le=="function"){var ke=B.value;J.payload=function(){return le(ke)},J.callback=function(){pF(P,B)}}var je=P.stateNode;return je!==null&&typeof je.componentDidCatch=="function"&&(J.callback=function(){pF(P,B),typeof le!="function"&&(GT===null?GT=new Set([this]):GT.add(this));var gt=B.stack;this.componentDidCatch(B.value,{componentStack:gt!==null?gt:""})}),J}function zy(P,B,J){var le=P.pingCache;if(le===null){le=P.pingCache=new rae;var ke=new Set;le.set(B,ke)}else ke=le.get(B),ke===void 0&&(ke=new Set,le.set(B,ke));ke.has(J)||(ke.add(J),P=uae.bind(null,P,B,J),B.then(P,P))}function IZ(P){do{var B;if((B=P.tag===13)&&(B=P.memoizedState,B=B!==null?B.dehydrated!==null:!0),B)return P;P=P.return}while(P!==null);return null}function AZ(P,B,J,le,ke){return(P.mode&1)===0?(P===B?P.flags|=65536:(P.flags|=128,J.flags|=131072,J.flags&=-52805,J.tag===1&&(J.alternate===null?J.tag=17:(B=ix(-1,1),B.tag=2,$y(J,B,1))),J.lanes|=1),P):(P.flags|=65536,P.lanes=ke,P)}var RZ=M.ReactCurrentOwner,kw=!1;function Tv(P,B,J,le){B.child=P===null?pZ(B,null,J,le):jT(B,P.child,J,le)}function MZ(P,B,J,le,ke){J=J.render;var je=B.ref;return Qm(B,ke),le=fO(P,B,J,le,je,ke),J=dk(),P!==null&&!kw?(B.updateQueue=P.updateQueue,B.flags&=-2053,P.lanes&=~ke,lx(P,B,ke)):(Rf&&J&&BV(B),B.flags|=1,Tv(P,B,le,ke),B.child)}function gF(P,B,J,le,ke){if(P===null){var je=J.type;return typeof je=="function"&&!T$(je)&&je.defaultProps===void 0&&J.compare===null&&J.defaultProps===void 0?(B.tag=15,B.type=je,OZ(P,B,je,le,ke)):(P=kF(J.type,null,le,B,B.mode,ke),P.ref=B.ref,P.return=B,B.child=P)}if(je=P.child,(P.lanes&ke)===0){var gt=je.memoizedProps;if(J=J.compare,J=J!==null?J:wo,J(gt,le)&&P.ref===B.ref)return lx(P,B,ke)}return B.flags|=1,P=pk(je,le),P.ref=B.ref,P.return=B,B.child=P}function OZ(P,B,J,le,ke){if(P!==null){var je=P.memoizedProps;if(wo(je,le)&&P.ref===B.ref)if(kw=!1,B.pendingProps=le=je,(P.lanes&ke)!==0)(P.flags&131072)!==0&&(kw=!0);else return B.lanes=P.lanes,lx(P,B,ke)}return s$(P,B,J,le,ke)}function NZ(P,B,J){var le=B.pendingProps,ke=le.children,je=P!==null?P.memoizedState:null;if(le.mode==="hidden")if((B.mode&1)===0)B.memoizedState={baseLanes:0,cachePool:null,transitions:null},Td(PI,Uy),Uy|=J;else{if((J&1073741824)===0)return P=je!==null?je.baseLanes|J:J,B.lanes=B.childLanes=1073741824,B.memoizedState={baseLanes:P,cachePool:null,transitions:null},B.updateQueue=null,Td(PI,Uy),Uy|=P,null;B.memoizedState={baseLanes:0,cachePool:null,transitions:null},le=je!==null?je.baseLanes:J,Td(PI,Uy),Uy|=le}else je!==null?(le=je.baseLanes|J,B.memoizedState=null):le=J,Td(PI,Uy),Uy|=le;return Tv(P,B,ke,J),B.child}function kn(P,B){var J=B.ref;(P===null&&J!==null||P!==null&&P.ref!==J)&&(B.flags|=512,B.flags|=2097152)}function s$(P,B,J,le,ke){var je=Sb(J)?F8:k0.current;return je=yI(B,je),Qm(B,ke),J=fO(P,B,J,le,je,ke),le=dk(),P!==null&&!kw?(B.updateQueue=P.updateQueue,B.flags&=-2053,P.lanes&=~ke,lx(P,B,ke)):(Rf&&le&&BV(B),B.flags|=1,Tv(P,B,J,ke),B.child)}function PZ(P,B,J,le,ke){if(Sb(J)){var je=!0;oO(B)}else je=!1;if(Qm(B,ke),B.stateNode===null)II(P,B),t$(B,J,le),i$(B,J,le,ke),le=!0;else if(P===null){var gt=B.stateNode,Qt=B.memoizedProps;gt.props=Qt;var wn=gt.context,xi=J.contextType;typeof xi=="object"&&xi!==null?xi=tm(xi):(xi=Sb(J)?F8:k0.current,xi=yI(B,xi));var Ar=J.getDerivedStateFromProps,pr=typeof Ar=="function"||typeof gt.getSnapshotBeforeUpdate=="function";pr||typeof gt.UNSAFE_componentWillReceiveProps!="function"&&typeof gt.componentWillReceiveProps!="function"||(Qt!==le||wn!==xi)&&n$(B,gt,le,xi),uk=!1;var xr=B.memoizedState;gt.state=xr,cO(B,le,gt,ke),wn=B.memoizedState,Qt!==le||xr!==wn||_w.current||uk?(typeof Ar=="function"&&(hF(B,J,Ar,le),wn=B.memoizedState),(Qt=uk||e$(B,J,Qt,le,xr,wn,xi))?(pr||typeof gt.UNSAFE_componentWillMount!="function"&&typeof gt.componentWillMount!="function"||(typeof gt.componentWillMount=="function"&>.componentWillMount(),typeof gt.UNSAFE_componentWillMount=="function"&>.UNSAFE_componentWillMount()),typeof gt.componentDidMount=="function"&&(B.flags|=4194308)):(typeof gt.componentDidMount=="function"&&(B.flags|=4194308),B.memoizedProps=le,B.memoizedState=wn),gt.props=le,gt.state=wn,gt.context=xi,le=Qt):(typeof gt.componentDidMount=="function"&&(B.flags|=4194308),le=!1)}else{gt=B.stateNode,gZ(P,B),Qt=B.memoizedProps,xi=B.type===B.elementType?Qt:z1(B.type,Qt),gt.props=xi,pr=B.pendingProps,xr=gt.context,wn=J.contextType,typeof wn=="object"&&wn!==null?wn=tm(wn):(wn=Sb(J)?F8:k0.current,wn=yI(B,wn));var Ro=J.getDerivedStateFromProps;(Ar=typeof Ro=="function"||typeof gt.getSnapshotBeforeUpdate=="function")||typeof gt.UNSAFE_componentWillReceiveProps!="function"&&typeof gt.componentWillReceiveProps!="function"||(Qt!==pr||xr!==wn)&&n$(B,gt,le,wn),uk=!1,xr=B.memoizedState,gt.state=xr,cO(B,le,gt,ke);var Xo=B.memoizedState;Qt!==pr||xr!==Xo||_w.current||uk?(typeof Ro=="function"&&(hF(B,J,Ro,le),Xo=B.memoizedState),(xi=uk||e$(B,J,xi,le,xr,Xo,wn)||!1)?(Ar||typeof gt.UNSAFE_componentWillUpdate!="function"&&typeof gt.componentWillUpdate!="function"||(typeof gt.componentWillUpdate=="function"&>.componentWillUpdate(le,Xo,wn),typeof gt.UNSAFE_componentWillUpdate=="function"&>.UNSAFE_componentWillUpdate(le,Xo,wn)),typeof gt.componentDidUpdate=="function"&&(B.flags|=4),typeof gt.getSnapshotBeforeUpdate=="function"&&(B.flags|=1024)):(typeof gt.componentDidUpdate!="function"||Qt===P.memoizedProps&&xr===P.memoizedState||(B.flags|=4),typeof gt.getSnapshotBeforeUpdate!="function"||Qt===P.memoizedProps&&xr===P.memoizedState||(B.flags|=1024),B.memoizedProps=le,B.memoizedState=Xo),gt.props=le,gt.state=Xo,gt.context=wn,le=xi):(typeof gt.componentDidUpdate!="function"||Qt===P.memoizedProps&&xr===P.memoizedState||(B.flags|=4),typeof gt.getSnapshotBeforeUpdate!="function"||Qt===P.memoizedProps&&xr===P.memoizedState||(B.flags|=1024),le=!1)}return o$(P,B,J,le,je,ke)}function o$(P,B,J,le,ke,je){kn(P,B);var gt=(B.flags&128)!==0;if(!le&&!gt)return ke&&FV(B,J,!1),lx(P,B,je);le=B.stateNode,RZ.current=B;var Qt=gt&&typeof J.getDerivedStateFromError!="function"?null:le.render();return B.flags|=1,P!==null&>?(B.child=jT(B,P.child,null,je),B.child=jT(B,null,Qt,je)):Tv(P,B,Qt,je),B.memoizedState=le.state,ke&&FV(B,J,!0),B.child}function FZ(P){var B=P.stateNode;B.pendingContext?hZ(P,B.pendingContext,B.pendingContext!==B.context):B.context&&hZ(P,B.context,!1),GV(P,B.containerInfo)}function jZ(P,B,J,le,ke){return FT(),Z7(ke),B.flags|=256,Tv(P,B,J,le),B.child}var a$={dehydrated:null,treeContext:null,retryLane:0};function l$(P){return{baseLanes:P,cachePool:null,transitions:null}}function HZ(P,B,J){var le=B.pendingProps,ke=rp.current,je=!1,gt=(B.flags&128)!==0,Qt;if((Qt=gt)||(Qt=P!==null&&P.memoizedState===null?!1:(ke&2)!==0),Qt?(je=!0,B.flags&=-129):(P===null||P.memoizedState!==null)&&(ke|=1),Td(rp,ke&1),P===null)return EI(B),P=B.memoizedState,P!==null&&(P=P.dehydrated,P!==null)?((B.mode&1)===0?B.lanes=1:P.data==="$!"?B.lanes=8:B.lanes=1073741824,null):(gt=le.children,P=le.fallback,je?(le=B.mode,je=B.child,gt={mode:"hidden",children:gt},(le&1)===0&&je!==null?(je.childLanes=0,je.pendingProps=gt):je=TF(gt,le,0,null),P=n5(P,le,J,null),je.return=B,P.return=B,je.sibling=P,B.child=je,B.child.memoizedState=l$(J),B.memoizedState=a$,P):c$(B,gt));if(ke=P.memoizedState,ke!==null&&(Qt=ke.dehydrated,Qt!==null))return u$(P,B,gt,le,Qt,ke,J);if(je){je=le.fallback,gt=B.mode,ke=P.child,Qt=ke.sibling;var wn={mode:"hidden",children:le.children};return(gt&1)===0&&B.child!==ke?(le=B.child,le.childLanes=0,le.pendingProps=wn,B.deletions=null):(le=pk(ke,wn),le.subtreeFlags=ke.subtreeFlags&14680064),Qt!==null?je=pk(Qt,je):(je=n5(je,gt,J,null),je.flags|=2),je.return=B,le.return=B,le.sibling=je,B.child=le,le=je,je=B.child,gt=P.child.memoizedState,gt=gt===null?l$(J):{baseLanes:gt.baseLanes|J,cachePool:null,transitions:gt.transitions},je.memoizedState=gt,je.childLanes=P.childLanes&~J,B.memoizedState=a$,le}return je=P.child,P=je.sibling,le=pk(je,{mode:"visible",children:le.children}),(B.mode&1)===0&&(le.lanes=J),le.return=B,le.sibling=null,P!==null&&(J=B.deletions,J===null?(B.deletions=[P],B.flags|=16):J.push(P)),B.child=le,B.memoizedState=null,le}function c$(P,B){return B=TF({mode:"visible",children:B},P.mode,0,null),B.return=P,P.child=B}function mF(P,B,J,le){return le!==null&&Z7(le),jT(B,P.child,null,J),P=c$(B,B.pendingProps.children),P.flags|=2,B.memoizedState=null,P}function u$(P,B,J,le,ke,je,gt){if(J)return B.flags&256?(B.flags&=-257,le=r$(Error(t(422))),mF(P,B,gt,le)):B.memoizedState!==null?(B.child=P.child,B.flags|=128,null):(je=le.fallback,ke=B.mode,le=TF({mode:"visible",children:le.children},ke,0,null),je=n5(je,ke,gt,null),je.flags|=2,le.return=B,je.return=B,le.sibling=je,B.child=le,(B.mode&1)!==0&&jT(B,P.child,null,gt),B.child.memoizedState=l$(gt),B.memoizedState=a$,je);if((B.mode&1)===0)return mF(P,B,gt,null);if(ke.data==="$!"){if(le=ke.nextSibling&&ke.nextSibling.dataset,le)var Qt=le.dgst;return le=Qt,je=Error(t(419)),le=r$(je,le,void 0),mF(P,B,gt,le)}if(Qt=(gt&P.childLanes)!==0,kw||Qt){if(le=U1,le!==null){switch(gt&-gt){case 4:ke=2;break;case 16:ke=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:ke=32;break;case 536870912:ke=268435456;break;default:ke=0}ke=(ke&(le.suspendedLanes|gt))!==0?0:ke,ke!==0&&ke!==je.retryLane&&(je.retryLane=ke,Ew(P,ke),DC(le,P,ke,-1))}return Iw(),le=r$(Error(t(421))),mF(P,B,gt,le)}return ke.data==="$?"?(B.flags|=128,B.child=P.child,B=k$.bind(null,P),ke._reactRetry=B,null):(P=je.treeContext,xb=Tr(ke.nextSibling),Wy=B,Rf=!0,Vy=null,P!==null&&(Cw[i2++]=jl,Cw[i2++]=lk,Cw[i2++]=ak,jl=P.id,lk=P.overflow,ak=B),B=c$(B,le.children),B.flags|=4096,B)}function BZ(P,B,J){P.lanes|=B;var le=P.alternate;le!==null&&(le.lanes|=B),nF(P.return,B,J)}function d$(P,B,J,le,ke){var je=P.memoizedState;je===null?P.memoizedState={isBackwards:B,rendering:null,renderingStartTime:0,last:le,tail:J,tailMode:ke}:(je.isBackwards=B,je.rendering=null,je.renderingStartTime=0,je.last=le,je.tail=J,je.tailMode=ke)}function WZ(P,B,J){var le=B.pendingProps,ke=le.revealOrder,je=le.tail;if(Tv(P,B,le.children,J),le=rp.current,(le&2)!==0)le=le&1|2,B.flags|=128;else{if(P!==null&&(P.flags&128)!==0)e:for(P=B.child;P!==null;){if(P.tag===13)P.memoizedState!==null&&BZ(P,J,B);else if(P.tag===19)BZ(P,J,B);else if(P.child!==null){P.child.return=P,P=P.child;continue}if(P===B)break e;for(;P.sibling===null;){if(P.return===null||P.return===B)break e;P=P.return}P.sibling.return=P.return,P=P.sibling}le&=1}if(Td(rp,le),(B.mode&1)===0)B.memoizedState=null;else switch(ke){case"forwards":for(J=B.child,ke=null;J!==null;)P=J.alternate,P!==null&&dO(P)===null&&(ke=J),J=J.sibling;J=ke,J===null?(ke=B.child,B.child=null):(ke=J.sibling,J.sibling=null),d$(B,!1,ke,J,je);break;case"backwards":for(J=null,ke=B.child,B.child=null;ke!==null;){if(P=ke.alternate,P!==null&&dO(P)===null){B.child=ke;break}P=ke.sibling,ke.sibling=J,J=ke,ke=P}d$(B,!0,J,null,je);break;case"together":d$(B,!1,null,null,void 0);break;default:B.memoizedState=null}return B.child}function II(P,B){(B.mode&1)===0&&P!==null&&(P.alternate=null,B.alternate=null,B.flags|=2)}function lx(P,B,J){if(P!==null&&(B.dependencies=P.dependencies),X8|=B.lanes,(J&B.childLanes)===0)return null;if(P!==null&&B.child!==P.child)throw Error(t(153));if(B.child!==null){for(P=B.child,J=pk(P,P.pendingProps),B.child=J,J.return=B;P.sibling!==null;)P=P.sibling,J=J.sibling=pk(P,P.pendingProps),J.return=B;J.sibling=null}return B.child}function VZ(P,B,J){switch(B.tag){case 3:FZ(B),FT();break;case 5:uO(B);break;case 1:Sb(B.type)&&oO(B);break;case 4:GV(B,B.stateNode.containerInfo);break;case 10:var le=B.type._context,ke=B.memoizedProps.value;Td(Q7,le._currentValue),le._currentValue=ke;break;case 13:if(le=B.memoizedState,le!==null)return le.dehydrated!==null?(Td(rp,rp.current&1),B.flags|=128,null):(J&B.child.childLanes)!==0?HZ(P,B,J):(Td(rp,rp.current&1),P=lx(P,B,J),P!==null?P.sibling:null);Td(rp,rp.current&1);break;case 19:if(le=(J&B.childLanes)!==0,(P.flags&128)!==0){if(le)return WZ(P,B,J);B.flags|=128}if(ke=B.memoizedState,ke!==null&&(ke.rendering=null,ke.tail=null,ke.lastEffect=null),Td(rp,rp.current),le)break;return null;case 22:case 23:return B.lanes=0,NZ(P,B,J)}return lx(P,B,J)}var wO,Y8,$Z,AI;wO=function(P,B){for(var J=B.child;J!==null;){if(J.tag===5||J.tag===6)P.appendChild(J.stateNode);else if(J.tag!==4&&J.child!==null){J.child.return=J,J=J.child;continue}if(J===B)break;for(;J.sibling===null;){if(J.return===null||J.return===B)return;J=J.return}J.sibling.return=J.return,J=J.sibling}},Y8=function(){},$Z=function(P,B,J,le){var ke=P.memoizedProps;if(ke!==le){P=B.stateNode,rx(xC.current);var je=null;switch(J){case"input":ke=lt(P,ke),le=lt(P,le),je=[];break;case"select":ke=de({},ke,{value:void 0}),le=de({},le,{value:void 0}),je=[];break;case"textarea":ke=tt(P,ke),le=tt(P,le),je=[];break;default:typeof ke.onClick!="function"&&typeof le.onClick=="function"&&(P.onclick=wI)}rn(J,le);var gt;J=null;for(xi in ke)if(!le.hasOwnProperty(xi)&&ke.hasOwnProperty(xi)&&ke[xi]!=null)if(xi==="style"){var Qt=ke[xi];for(gt in Qt)Qt.hasOwnProperty(gt)&&(J||(J={}),J[gt]="")}else xi!=="dangerouslySetInnerHTML"&&xi!=="children"&&xi!=="suppressContentEditableWarning"&&xi!=="suppressHydrationWarning"&&xi!=="autoFocus"&&(r.hasOwnProperty(xi)?je||(je=[]):(je=je||[]).push(xi,null));for(xi in le){var wn=le[xi];if(Qt=ke?.[xi],le.hasOwnProperty(xi)&&wn!==Qt&&(wn!=null||Qt!=null))if(xi==="style")if(Qt){for(gt in Qt)!Qt.hasOwnProperty(gt)||wn&&wn.hasOwnProperty(gt)||(J||(J={}),J[gt]="");for(gt in wn)wn.hasOwnProperty(gt)&&Qt[gt]!==wn[gt]&&(J||(J={}),J[gt]=wn[gt])}else J||(je||(je=[]),je.push(xi,J)),J=wn;else xi==="dangerouslySetInnerHTML"?(wn=wn?wn.__html:void 0,Qt=Qt?Qt.__html:void 0,wn!=null&&Qt!==wn&&(je=je||[]).push(xi,wn)):xi==="children"?typeof wn!="string"&&typeof wn!="number"||(je=je||[]).push(xi,""+wn):xi!=="suppressContentEditableWarning"&&xi!=="suppressHydrationWarning"&&(r.hasOwnProperty(xi)?(wn!=null&&xi==="onScroll"&&Kh("scroll",P),je||Qt===wn||(je=[])):(je=je||[]).push(xi,wn))}J&&(je=je||[]).push("style",J);var xi=je;(B.updateQueue=xi)&&(B.flags|=4)}},AI=function(P,B,J,le){J!==le&&(B.flags|=4)};function UT(P,B){if(!Rf)switch(P.tailMode){case"hidden":B=P.tail;for(var J=null;B!==null;)B.alternate!==null&&(J=B),B=B.sibling;J===null?P.tail=null:J.sibling=null;break;case"collapsed":J=P.tail;for(var le=null;J!==null;)J.alternate!==null&&(le=J),J=J.sibling;le===null?B||P.tail===null?P.tail=null:P.tail.sibling=null:le.sibling=null}}function kb(P){var B=P.alternate!==null&&P.alternate.child===P.child,J=0,le=0;if(B)for(var ke=P.child;ke!==null;)J|=ke.lanes|ke.childLanes,le|=ke.subtreeFlags&14680064,le|=ke.flags&14680064,ke.return=P,ke=ke.sibling;else for(ke=P.child;ke!==null;)J|=ke.lanes|ke.childLanes,le|=ke.subtreeFlags,le|=ke.flags,ke.return=P,ke=ke.sibling;return P.subtreeFlags|=le,P.childLanes=J,B}function sae(P,B,J){var le=B.pendingProps;switch(Sw(B),B.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return kb(B),null;case 1:return Sb(B.type)&&_I(),kb(B),null;case 3:return le=B.stateNode,U8(),Yd(_w),Yd(k0),sF(),le.pendingContext&&(le.context=le.pendingContext,le.pendingContext=null),(P===null||P.child===null)&&(B8(B)?B.flags|=4:P===null||P.memoizedState.isDehydrated&&(B.flags&256)===0||(B.flags|=1024,Vy!==null&&(S$(Vy),Vy=null))),Y8(P,B),kb(B),null;case 5:rF(B);var ke=rx(z8.current);if(J=B.type,P!==null&&B.stateNode!=null)$Z(P,B,J,le,ke),P.ref!==B.ref&&(B.flags|=512,B.flags|=2097152);else{if(!le){if(B.stateNode===null)throw Error(t(166));return kb(B),null}if(P=rx(xC.current),B8(B)){le=B.stateNode,J=B.type;var je=B.memoizedProps;switch(le[Uc]=B,le[Ho]=je,P=(B.mode&1)!==0,J){case"dialog":Kh("cancel",le),Kh("close",le);break;case"iframe":case"object":case"embed":Kh("load",le);break;case"video":case"audio":for(ke=0;ke<\/script>",P=P.removeChild(P.firstChild)):typeof le.is=="string"?P=gt.createElement(J,{is:le.is}):(P=gt.createElement(J),J==="select"&&(gt=P,le.multiple?gt.multiple=!0:le.size&&(gt.size=le.size))):P=gt.createElementNS(P,J),P[Uc]=B,P[Ho]=le,wO(P,B,!1,!1),B.stateNode=P;e:{switch(gt=xt(J,le),J){case"dialog":Kh("cancel",P),Kh("close",P),ke=le;break;case"iframe":case"object":case"embed":Kh("load",P),ke=le;break;case"video":case"audio":for(ke=0;keFI&&(B.flags|=128,le=!0,UT(je,!1),B.lanes=4194304)}else{if(!le)if(P=dO(gt),P!==null){if(B.flags|=128,le=!0,J=P.updateQueue,J!==null&&(B.updateQueue=J,B.flags|=4),UT(je,!0),je.tail===null&&je.tailMode==="hidden"&&!gt.alternate&&!Rf)return kb(B),null}else 2*Es()-je.renderingStartTime>FI&&J!==1073741824&&(B.flags|=128,le=!0,UT(je,!1),B.lanes=4194304);je.isBackwards?(gt.sibling=B.child,B.child=gt):(J=je.last,J!==null?J.sibling=gt:B.child=gt,je.last=gt)}return je.tail!==null?(B=je.tail,je.rendering=B,je.tail=B.sibling,je.renderingStartTime=Es(),B.sibling=null,J=rp.current,Td(rp,le?J&1|2:J&1),B):(kb(B),null);case 22:case 23:return xF(),le=B.memoizedState!==null,P!==null&&P.memoizedState!==null!==le&&(B.flags|=8192),le&&(B.mode&1)!==0?(Uy&1073741824)!==0&&(kb(B),B.subtreeFlags&6&&(B.flags|=8192)):kb(B),null;case 24:return null;case 25:return null}throw Error(t(156,B.tag))}function oae(P,B){switch(Sw(B),B.tag){case 1:return Sb(B.type)&&_I(),P=B.flags,P&65536?(B.flags=P&-65537|128,B):null;case 3:return U8(),Yd(_w),Yd(k0),sF(),P=B.flags,(P&65536)!==0&&(P&128)===0?(B.flags=P&-65537|128,B):null;case 5:return rF(B),null;case 13:if(Yd(rp),P=B.memoizedState,P!==null&&P.dehydrated!==null){if(B.alternate===null)throw Error(t(340));FT()}return P=B.flags,P&65536?(B.flags=P&-65537|128,B):null;case 19:return Yd(rp),null;case 4:return U8(),null;case 10:return tF(B.type._context),null;case 22:case 23:return xF(),null;case 24:return null;default:return null}}var bF=!1,Tb=!1,aae=typeof WeakSet=="function"?WeakSet:Set,Bo=null;function RI(P,B){var J=P.ref;if(J!==null)if(typeof J=="function")try{J(null)}catch(le){Og(P,B,le)}else J.current=null}function h$(P,B,J){try{J()}catch(le){Og(P,B,le)}}var zZ=!1;function vF(P,B){if(Ve=bC,P=fI(),QM(P)){if("selectionStart"in P)var J={start:P.selectionStart,end:P.selectionEnd};else e:{J=(J=P.ownerDocument)&&J.defaultView||window;var le=J.getSelection&&J.getSelection();if(le&&le.rangeCount!==0){J=le.anchorNode;var ke=le.anchorOffset,je=le.focusNode;le=le.focusOffset;try{J.nodeType,je.nodeType}catch{J=null;break e}var gt=0,Qt=-1,wn=-1,xi=0,Ar=0,pr=P,xr=null;t:for(;;){for(var Ro;pr!==J||ke!==0&&pr.nodeType!==3||(Qt=gt+ke),pr!==je||le!==0&&pr.nodeType!==3||(wn=gt+le),pr.nodeType===3&&(gt+=pr.nodeValue.length),(Ro=pr.firstChild)!==null;)xr=pr,pr=Ro;for(;;){if(pr===P)break t;if(xr===J&&++xi===ke&&(Qt=gt),xr===je&&++Ar===le&&(wn=gt),(Ro=pr.nextSibling)!==null)break;pr=xr,xr=pr.parentNode}pr=Ro}J=Qt===-1||wn===-1?null:{start:Qt,end:wn}}else J=null}J=J||{start:0,end:0}}else J=null;for(ae={focusedElem:P,selectionRange:J},bC=!1,Bo=B;Bo!==null;)if(B=Bo,P=B.child,(B.subtreeFlags&1028)!==0&&P!==null)P.return=B,Bo=P;else for(;Bo!==null;){B=Bo;try{var Xo=B.alternate;if((B.flags&1024)!==0)switch(B.tag){case 0:case 11:case 15:break;case 1:if(Xo!==null){var Qo=Xo.memoizedProps,nm=Xo.memoizedState,si=B.stateNode,On=si.getSnapshotBeforeUpdate(B.elementType===B.type?Qo:z1(B.type,Qo),nm);si.__reactInternalSnapshotBeforeUpdate=On}break;case 3:var pi=B.stateNode.containerInfo;pi.nodeType===1?pi.textContent="":pi.nodeType===9&&pi.documentElement&&pi.removeChild(pi.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(t(163))}}catch(ws){Og(B,B.return,ws)}if(P=B.sibling,P!==null){P.return=B.return,Bo=P;break}Bo=B.return}return Xo=zZ,zZ=!1,Xo}function MI(P,B,J){var le=B.updateQueue;if(le=le!==null?le.lastEffect:null,le!==null){var ke=le=le.next;do{if((ke.tag&P)===P){var je=ke.destroy;ke.destroy=void 0,je!==void 0&&h$(B,J,je)}ke=ke.next}while(ke!==le)}}function OI(P,B){if(B=B.updateQueue,B=B!==null?B.lastEffect:null,B!==null){var J=B=B.next;do{if((J.tag&P)===P){var le=J.create;J.destroy=le()}J=J.next}while(J!==B)}}function f$(P){var B=P.ref;if(B!==null){var J=P.stateNode;P.tag,P=J,typeof B=="function"?B(P):B.current=P}}function Z8(P){var B=P.alternate;B!==null&&(P.alternate=null,Z8(B)),P.child=null,P.deletions=null,P.sibling=null,P.tag===5&&(B=P.stateNode,B!==null&&(delete B[Uc],delete B[Ho],delete B[Ev],delete B[e2],delete B[AT])),P.stateNode=null,P.return=null,P.dependencies=null,P.memoizedProps=null,P.memoizedState=null,P.pendingProps=null,P.stateNode=null,P.updateQueue=null}function p$(P){return P.tag===5||P.tag===3||P.tag===4}function UZ(P){e:for(;;){for(;P.sibling===null;){if(P.return===null||p$(P.return))return null;P=P.return}for(P.sibling.return=P.return,P=P.sibling;P.tag!==5&&P.tag!==6&&P.tag!==18;){if(P.flags&2||P.child===null||P.tag===4)continue e;P.child.return=P,P=P.child}if(!(P.flags&2))return P.stateNode}}function g$(P,B,J){var le=P.tag;if(le===5||le===6)P=P.stateNode,B?J.nodeType===8?J.parentNode.insertBefore(P,B):J.insertBefore(P,B):(J.nodeType===8?(B=J.parentNode,B.insertBefore(P,J)):(B=J,B.appendChild(P)),J=J._reactRootContainer,J!=null||B.onclick!==null||(B.onclick=wI));else if(le!==4&&(P=P.child,P!==null))for(g$(P,B,J),P=P.sibling;P!==null;)g$(P,B,J),P=P.sibling}function m$(P,B,J){var le=P.tag;if(le===5||le===6)P=P.stateNode,B?J.insertBefore(P,B):J.appendChild(P);else if(le!==4&&(P=P.child,P!==null))for(m$(P,B,J),P=P.sibling;P!==null;)m$(P,B,J),P=P.sibling}var T0=null,cx=!1;function qT(P,B,J){for(J=J.child;J!==null;)qZ(P,B,J),J=J.sibling}function qZ(P,B,J){if(cd&&typeof cd.onCommitFiberUnmount=="function")try{cd.onCommitFiberUnmount(Pu,J)}catch{}switch(J.tag){case 5:Tb||RI(J,B);case 6:var le=T0,ke=cx;T0=null,qT(P,B,J),T0=le,cx=ke,T0!==null&&(cx?(P=T0,J=J.stateNode,P.nodeType===8?P.parentNode.removeChild(J):P.removeChild(J)):T0.removeChild(J.stateNode));break;case 18:T0!==null&&(cx?(P=T0,J=J.stateNode,P.nodeType===8?Qn(P.parentNode,J):P.nodeType===1&&Qn(P,J),Mg(P)):Qn(T0,J.stateNode));break;case 4:le=T0,ke=cx,T0=J.stateNode.containerInfo,cx=!0,qT(P,B,J),T0=le,cx=ke;break;case 0:case 11:case 14:case 15:if(!Tb&&(le=J.updateQueue,le!==null&&(le=le.lastEffect,le!==null))){ke=le=le.next;do{var je=ke,gt=je.destroy;je=je.tag,gt!==void 0&&((je&2)!==0||(je&4)!==0)&&h$(J,B,gt),ke=ke.next}while(ke!==le)}qT(P,B,J);break;case 1:if(!Tb&&(RI(J,B),le=J.stateNode,typeof le.componentWillUnmount=="function"))try{le.props=J.memoizedProps,le.state=J.memoizedState,le.componentWillUnmount()}catch(Qt){Og(J,B,Qt)}qT(P,B,J);break;case 21:qT(P,B,J);break;case 22:J.mode&1?(Tb=(le=Tb)||J.memoizedState!==null,qT(P,B,J),Tb=le):qT(P,B,J);break;default:qT(P,B,J)}}function GZ(P){var B=P.updateQueue;if(B!==null){P.updateQueue=null;var J=P.stateNode;J===null&&(J=P.stateNode=new aae),B.forEach(function(le){var ke=dae.bind(null,P,le);J.has(le)||(J.add(le),le.then(ke,ke))})}}function kC(P,B){var J=B.deletions;if(J!==null)for(var le=0;leke&&(ke=gt),le&=~je}if(le=ke,le=Es()-le,le=(120>le?120:480>le?480:1080>le?1080:1920>le?1920:3e3>le?3e3:4320>le?4320:1960*NI(le/1960))-le,10P?16:P,fk===null)var le=!1;else{if(P=fk,fk=null,Q8=0,(dd&6)!==0)throw Error(t(331));var ke=dd;for(dd|=4,Bo=P.current;Bo!==null;){var je=Bo,gt=je.child;if((Bo.flags&16)!==0){var Qt=je.deletions;if(Qt!==null){for(var wn=0;wnEs()-_$?Dv(P,0):Lb|=J),Dw(P,B)}function eX(P,B){B===0&&((P.mode&1)===0?B=1:(B=Fl,Fl<<=1,(Fl&130023424)===0&&(Fl=4194304)));var J=Lv();P=Ew(P,B),P!==null&&(gC(P,B,J),Dw(P,J))}function k$(P){var B=P.memoizedState,J=0;B!==null&&(J=B.retryLane),eX(P,J)}function dae(P,B){var J=0;switch(P.tag){case 13:var le=P.stateNode,ke=P.memoizedState;ke!==null&&(J=ke.retryLane);break;case 19:le=P.stateNode;break;default:throw Error(t(314))}le!==null&&le.delete(B),eX(P,J)}var EF;EF=function(P,B,J){if(P!==null)if(P.memoizedProps!==B.pendingProps||_w.current)kw=!0;else{if((P.lanes&J)===0&&(B.flags&128)===0)return kw=!1,VZ(P,B,J);kw=(P.flags&131072)!==0}else kw=!1,Rf&&(B.flags&1048576)!==0&&xI(B,aO,B.index);switch(B.lanes=0,B.tag){case 2:var le=B.type;II(P,B),P=B.pendingProps;var ke=yI(B,k0.current);Qm(B,J),ke=fO(null,B,le,P,ke,J);var je=dk();return B.flags|=1,typeof ke=="object"&&ke!==null&&typeof ke.render=="function"&&ke.$$typeof===void 0?(B.tag=1,B.memoizedState=null,B.updateQueue=null,Sb(le)?(je=!0,oO(B)):je=!1,B.memoizedState=ke.state!==null&&ke.state!==void 0?ke.state:null,iF(B),ke.updater=fF,B.stateNode=ke,ke._reactInternals=B,i$(B,le,P,J),B=o$(null,B,le,!0,je,J)):(B.tag=0,Rf&&je&&BV(B),Tv(null,B,ke,J),B=B.child),B;case 16:le=B.elementType;e:{switch(II(P,B),P=B.pendingProps,ke=le._init,le=ke(le._payload),B.type=le,ke=B.tag=hae(le),P=z1(le,P),ke){case 0:B=s$(null,B,le,P,J);break e;case 1:B=PZ(null,B,le,P,J);break e;case 11:B=MZ(null,B,le,P,J);break e;case 14:B=gF(null,B,le,z1(le.type,P),J);break e}throw Error(t(306,le,""))}return B;case 0:return le=B.type,ke=B.pendingProps,ke=B.elementType===le?ke:z1(le,ke),s$(P,B,le,ke,J);case 1:return le=B.type,ke=B.pendingProps,ke=B.elementType===le?ke:z1(le,ke),PZ(P,B,le,ke,J);case 3:e:{if(FZ(B),P===null)throw Error(t(387));le=B.pendingProps,je=B.memoizedState,ke=je.element,gZ(P,B),cO(B,le,null,J);var gt=B.memoizedState;if(le=gt.element,je.isDehydrated)if(je={element:le,isDehydrated:!1,cache:gt.cache,pendingSuspenseBoundaries:gt.pendingSuspenseBoundaries,transitions:gt.transitions},B.updateQueue.baseState=je,B.memoizedState=je,B.flags&256){ke=DI(Error(t(423)),B),B=jZ(P,B,le,J,ke);break e}else if(le!==ke){ke=DI(Error(t(424)),B),B=jZ(P,B,le,J,ke);break e}else for(xb=Tr(B.stateNode.containerInfo.firstChild),Wy=B,Rf=!0,Vy=null,J=pZ(B,null,le,J),B.child=J;J;)J.flags=J.flags&-3|4096,J=J.sibling;else{if(FT(),le===ke){B=lx(P,B,J);break e}Tv(P,B,le,J)}B=B.child}return B;case 5:return uO(B),P===null&&EI(B),le=B.type,ke=B.pendingProps,je=P!==null?P.memoizedProps:null,gt=ke.children,Oe(le,ke)?gt=null:je!==null&&Oe(le,je)&&(B.flags|=32),kn(P,B),Tv(P,B,gt,J),B.child;case 6:return P===null&&EI(B),null;case 13:return HZ(P,B,J);case 4:return GV(B,B.stateNode.containerInfo),le=B.pendingProps,P===null?B.child=jT(B,null,le,J):Tv(P,B,le,J),B.child;case 11:return le=B.type,ke=B.pendingProps,ke=B.elementType===le?ke:z1(le,ke),MZ(P,B,le,ke,J);case 7:return Tv(P,B,B.pendingProps,J),B.child;case 8:return Tv(P,B,B.pendingProps.children,J),B.child;case 12:return Tv(P,B,B.pendingProps.children,J),B.child;case 10:e:{if(le=B.type._context,ke=B.pendingProps,je=B.memoizedProps,gt=ke.value,Td(Q7,le._currentValue),le._currentValue=gt,je!==null)if(Je(je.value,gt)){if(je.children===ke.children&&!_w.current){B=lx(P,B,J);break e}}else for(je=B.child,je!==null&&(je.return=B);je!==null;){var Qt=je.dependencies;if(Qt!==null){gt=je.child;for(var wn=Qt.firstContext;wn!==null;){if(wn.context===le){if(je.tag===1){wn=ix(-1,J&-J),wn.tag=2;var xi=je.updateQueue;if(xi!==null){xi=xi.shared;var Ar=xi.pending;Ar===null?wn.next=wn:(wn.next=Ar.next,Ar.next=wn),xi.pending=wn}}je.lanes|=J,wn=je.alternate,wn!==null&&(wn.lanes|=J),nF(je.return,J,B),Qt.lanes|=J;break}wn=wn.next}}else if(je.tag===10)gt=je.type===B.type?null:je.child;else if(je.tag===18){if(gt=je.return,gt===null)throw Error(t(341));gt.lanes|=J,Qt=gt.alternate,Qt!==null&&(Qt.lanes|=J),nF(gt,J,B),gt=je.sibling}else gt=je.child;if(gt!==null)gt.return=je;else for(gt=je;gt!==null;){if(gt===B){gt=null;break}if(je=gt.sibling,je!==null){je.return=gt.return,gt=je;break}gt=gt.return}je=gt}Tv(P,B,ke.children,J),B=B.child}return B;case 9:return ke=B.type,le=B.pendingProps.children,Qm(B,J),ke=tm(ke),le=le(ke),B.flags|=1,Tv(P,B,le,J),B.child;case 14:return le=B.type,ke=z1(le,B.pendingProps),ke=z1(le.type,ke),gF(P,B,le,ke,J);case 15:return OZ(P,B,B.type,B.pendingProps,J);case 17:return le=B.type,ke=B.pendingProps,ke=B.elementType===le?ke:z1(le,ke),II(P,B),B.tag=1,Sb(le)?(P=!0,oO(B)):P=!1,Qm(B,J),t$(B,le,ke),i$(B,le,ke,J),o$(null,B,le,!0,P,J);case 19:return WZ(P,B,J);case 22:return NZ(P,B,J)}throw Error(t(156,B.tag))};function t1(P,B){return Cn(P,B)}function Aw(P,B,J,le){this.tag=P,this.key=J,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=B,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=le,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function l2(P,B,J,le){return new Aw(P,B,J,le)}function T$(P){return P=P.prototype,!(!P||!P.isReactComponent)}function hae(P){if(typeof P=="function")return T$(P)?1:0;if(P!=null){if(P=P.$$typeof,P===G)return 11;if(P===ie)return 14}return 2}function pk(P,B){var J=P.alternate;return J===null?(J=l2(P.tag,B,P.key,P.mode),J.elementType=P.elementType,J.type=P.type,J.stateNode=P.stateNode,J.alternate=P,P.alternate=J):(J.pendingProps=B,J.type=P.type,J.flags=0,J.subtreeFlags=0,J.deletions=null),J.flags=P.flags&14680064,J.childLanes=P.childLanes,J.lanes=P.lanes,J.child=P.child,J.memoizedProps=P.memoizedProps,J.memoizedState=P.memoizedState,J.updateQueue=P.updateQueue,B=P.dependencies,J.dependencies=B===null?null:{lanes:B.lanes,firstContext:B.firstContext},J.sibling=P.sibling,J.index=P.index,J.ref=P.ref,J}function kF(P,B,J,le,ke,je){var gt=2;if(le=P,typeof P=="function")T$(P)&&(gt=1);else if(typeof P=="string")gt=5;else e:switch(P){case j:return n5(J.children,ke,je,B);case W:gt=8,ke|=8;break;case U:return P=l2(12,J,B,ke|2),P.elementType=U,P.lanes=je,P;case ee:return P=l2(13,J,B,ke),P.elementType=ee,P.lanes=je,P;case Q:return P=l2(19,J,B,ke),P.elementType=Q,P.lanes=je,P;case ue:return TF(J,ke,je,B);default:if(typeof P=="object"&&P!==null)switch(P.$$typeof){case Z:gt=10;break e;case te:gt=9;break e;case G:gt=11;break e;case ie:gt=14;break e;case se:gt=16,le=null;break e}throw Error(t(130,P==null?P:typeof P,""))}return B=l2(gt,J,B,ke),B.elementType=P,B.type=le,B.lanes=je,B}function n5(P,B,J,le){return P=l2(7,P,le,B),P.lanes=J,P}function TF(P,B,J,le){return P=l2(22,P,le,B),P.elementType=ue,P.lanes=J,P.stateNode={isHidden:!1},P}function L$(P,B,J){return P=l2(6,P,null,B),P.lanes=J,P}function D$(P,B,J){return B=l2(4,P.children!==null?P.children:[],P.key,B),B.lanes=J,B.stateNode={containerInfo:P.containerInfo,pendingChildren:null,implementation:P.implementation},B}function fae(P,B,J,le,ke){this.tag=B,this.containerInfo=P,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=GE(0),this.expirationTimes=GE(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=GE(0),this.identifierPrefix=le,this.onRecoverableError=ke,this.mutableSourceEagerHydrationData=null}function I$(P,B,J,le,ke,je,gt,Qt,wn){return P=new fae(P,B,J,Qt,wn),B===1?(B=1,je===!0&&(B|=8)):B=0,je=l2(3,null,null,B),P.current=je,je.stateNode=P,je.memoizedState={element:le,isDehydrated:J,cache:null,transitions:null,pendingSuspenseBoundaries:null},iF(je),P}function HI(P,B,J){var le=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}return n(),REt.exports=cpr(),REt.exports}var KR=zzt();const Ofe=Vs(KR),lPn={disabled:!1},iGe=Rt.createContext(null);var Xci=function(e){return e.scrollTop},dke="unmounted",qte="exited",Gte="entering",hfe="entered",UOt="exiting",o8=(function(n){YW(e,n);function e(i,r){var o;o=n.call(this,i,r)||this;var l=r,c=l&&!l.isMounting?i.enter:i.appear,d;return o.appearStatus=null,i.in?c?(d=qte,o.appearStatus=Gte):d=hfe:i.unmountOnExit||i.mountOnEnter?d=dke:d=qte,o.state={status:d},o.nextCallback=null,o}e.getDerivedStateFromProps=function(r,o){var l=r.in;return l&&o.status===dke?{status:qte}:null};var t=e.prototype;return t.componentDidMount=function(){this.updateStatus(!0,this.appearStatus)},t.componentDidUpdate=function(r){var o=null;if(r!==this.props){var l=this.state.status;this.props.in?l!==Gte&&l!==hfe&&(o=Gte):(l===Gte||l===hfe)&&(o=UOt)}this.updateStatus(!1,o)},t.componentWillUnmount=function(){this.cancelNextCallback()},t.getTimeouts=function(){var r=this.props.timeout,o,l,c;return o=l=c=r,r!=null&&typeof r!="number"&&(o=r.exit,l=r.enter,c=r.appear!==void 0?r.appear:l),{exit:o,enter:l,appear:c}},t.updateStatus=function(r,o){if(r===void 0&&(r=!1),o!==null)if(this.cancelNextCallback(),o===Gte){if(this.props.unmountOnExit||this.props.mountOnEnter){var l=this.props.nodeRef?this.props.nodeRef.current:Ofe.findDOMNode(this);l&&Xci(l)}this.performEnter(r)}else this.performExit();else this.props.unmountOnExit&&this.state.status===qte&&this.setState({status:dke})},t.performEnter=function(r){var o=this,l=this.props.enter,c=this.context?this.context.isMounting:r,d=this.props.nodeRef?[c]:[Ofe.findDOMNode(this),c],h=d[0],p=d[1],m=this.getTimeouts(),b=c?m.appear:m.enter;if(!r&&!l||lPn.disabled){this.safeSetState({status:hfe},function(){o.props.onEntered(h)});return}this.props.onEnter(h,p),this.safeSetState({status:Gte},function(){o.props.onEntering(h,p),o.onTransitionEnd(b,function(){o.safeSetState({status:hfe},function(){o.props.onEntered(h,p)})})})},t.performExit=function(){var r=this,o=this.props.exit,l=this.getTimeouts(),c=this.props.nodeRef?void 0:Ofe.findDOMNode(this);if(!o||lPn.disabled){this.safeSetState({status:qte},function(){r.props.onExited(c)});return}this.props.onExit(c),this.safeSetState({status:UOt},function(){r.props.onExiting(c),r.onTransitionEnd(l.exit,function(){r.safeSetState({status:qte},function(){r.props.onExited(c)})})})},t.cancelNextCallback=function(){this.nextCallback!==null&&(this.nextCallback.cancel(),this.nextCallback=null)},t.safeSetState=function(r,o){o=this.setNextCallback(o),this.setState(r,o)},t.setNextCallback=function(r){var o=this,l=!0;return this.nextCallback=function(c){l&&(l=!1,o.nextCallback=null,r(c))},this.nextCallback.cancel=function(){l=!1},this.nextCallback},t.onTransitionEnd=function(r,o){this.setNextCallback(o);var l=this.props.nodeRef?this.props.nodeRef.current:Ofe.findDOMNode(this),c=r==null&&!this.props.addEndListener;if(!l||c){setTimeout(this.nextCallback,0);return}if(this.props.addEndListener){var d=this.props.nodeRef?[this.nextCallback]:[l,this.nextCallback],h=d[0],p=d[1];this.props.addEndListener(h,p)}r!=null&&setTimeout(this.nextCallback,r)},t.render=function(){var r=this.state.status;if(r===dke)return null;var o=this.props,l=o.children;o.in,o.mountOnEnter,o.unmountOnExit,o.appear,o.enter,o.exit,o.timeout,o.addEndListener,o.onEnter,o.onEntering,o.onEntered,o.onExit,o.onExiting,o.onExited,o.nodeRef;var c=dc(o,["children","in","mountOnEnter","unmountOnExit","appear","enter","exit","timeout","addEndListener","onEnter","onEntering","onEntered","onExit","onExiting","onExited","nodeRef"]);return Rt.createElement(iGe.Provider,{value:null},typeof l=="function"?l(r,c):Rt.cloneElement(Rt.Children.only(l),c))},e})(Rt.Component);o8.contextType=iGe;o8.propTypes={};function she(){}o8.defaultProps={in:!1,mountOnEnter:!1,unmountOnExit:!1,appear:!1,enter:!0,exit:!0,onEnter:she,onEntering:she,onEntered:she,onExit:she,onExiting:she,onExited:she};o8.UNMOUNTED=dke;o8.EXITED=qte;o8.ENTERING=Gte;o8.ENTERED=hfe;o8.EXITING=UOt;var upr=function(e,t){return e&&t&&t.split(" ").forEach(function(i){return spr(e,i)})},NEt=function(e,t){return e&&t&&t.split(" ").forEach(function(i){return opr(e,i)})},Uzt=(function(n){YW(e,n);function e(){for(var i,r=arguments.length,o=new Array(r),l=0;l{this.currentId=null,t()},e)}clear=()=>{this.currentId!==null&&(clearTimeout(this.currentId),this.currentId=null)};disposeEffect=()=>this.clear}function PG(){const n=Qci(Wet.create).current;return bpr(n.disposeEffect),n}const Gzt=n=>n.scrollTop;function JK(n,e){const{timeout:t,easing:i,style:r={}}=n;return{duration:r.transitionDuration??(typeof t=="number"?t:t[e.mode]||0),easing:r.transitionTimingFunction??(typeof i=="object"?i[e.mode]:i),delay:r.transitionDelay}}function k9(n){return typeof n=="string"}function Jci(n,e,t){return n===void 0||k9(n)?e:{...e,ownerState:{...e.ownerState,...t}}}function eui(n,e,t){return typeof n=="function"?n(e,t):n}function bie(n,e=[]){if(n===void 0)return{};const t={};return Object.keys(n).filter(i=>i.match(/^on[A-Z]/)&&typeof n[i]=="function"&&!e.includes(i)).forEach(i=>{t[i]=n[i]}),t}function uPn(n){if(n===void 0)return{};const e={};return Object.keys(n).filter(t=>!(t.match(/^on[A-Z]/)&&typeof n[t]=="function")).forEach(t=>{e[t]=n[t]}),e}function tui(n){const{getSlotProps:e,additionalProps:t,externalSlotProps:i,externalForwardedProps:r,className:o}=n;if(!e){const w=_i(t?.className,o,r?.className,i?.className),_={...t?.style,...r?.style,...i?.style},x={...t,...r,...i};return w.length>0&&(x.className=w),Object.keys(_).length>0&&(x.style=_),{props:x,internalRef:void 0}}const l=bie({...r,...i}),c=uPn(i),d=uPn(r),h=e(l),p=_i(h?.className,t?.className,o,r?.className,i?.className),m={...h?.style,...t?.style,...r?.style,...i?.style},b={...h,...t,...d,...c};return p.length>0&&(b.className=p),Object.keys(m).length>0&&(b.style=m),{props:b,internalRef:h.ref}}function _o(n,e){const{className:t,elementType:i,ownerState:r,externalForwardedProps:o,internalForwardedProps:l,shouldForwardComponentProp:c=!1,...d}=e,{component:h,slots:p={[n]:void 0},slotProps:m={[n]:void 0},...b}=o,w=p[n]||i,_=eui(m[n],r),{props:{component:x,...T},internalRef:I}=tui({className:t,...d,externalForwardedProps:n==="root"?b:void 0,externalSlotProps:_}),D=xm(I,_?.ref,e.ref),A=n==="root"?x||h:x,M=Jci(w,{...n==="root"&&!h&&!p[n]&&l,...n!=="root"&&!p[n]&&l,...T,...A&&!c&&{as:A},...A&&c&&{component:A},ref:D},r);return[w,M]}function vpr(n){return Po("MuiCollapse",n)}Fo("MuiCollapse",["root","horizontal","vertical","entered","hidden","wrapper","wrapperInner"]);const wpr=n=>{const{orientation:e,classes:t}=n;return jo({root:["root",e],entered:["entered"],hidden:["hidden"],wrapper:["wrapper",e],wrapperInner:["wrapperInner",e]},vpr,t)},ypr=nn("div",{name:"MuiCollapse",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.orientation],t.state==="entered"&&e.entered,t.state==="exited"&&!t.in&&t.collapsedSize==="0px"&&e.hidden]}})(Gs(({theme:n})=>({height:0,overflow:"hidden",transition:n.transitions.create("height"),variants:[{props:{orientation:"horizontal"},style:{height:"auto",width:0,transition:n.transitions.create("width")}},{props:{state:"entered"},style:{height:"auto",overflow:"visible"}},{props:{state:"entered",orientation:"horizontal"},style:{width:"auto"}},{props:({ownerState:e})=>e.state==="exited"&&!e.in&&e.collapsedSize==="0px",style:{visibility:"hidden"}}]}))),_pr=nn("div",{name:"MuiCollapse",slot:"Wrapper"})({display:"flex",width:"100%",variants:[{props:{orientation:"horizontal"},style:{width:"auto",height:"100%"}}]}),Cpr=nn("div",{name:"MuiCollapse",slot:"WrapperInner"})({width:"100%",variants:[{props:{orientation:"horizontal"},style:{width:"auto",height:"100%"}}]}),d6e=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiCollapse"}),{addEndListener:r,children:o,className:l,collapsedSize:c="0px",component:d,easing:h,in:p,onEnter:m,onEntered:b,onEntering:w,onExit:_,onExited:x,onExiting:T,orientation:I="vertical",slots:D={},slotProps:A={},style:M,timeout:O=Tci.standard,TransitionComponent:F=o8,...j}=i,W={...i,orientation:I,collapsedSize:c},U=wpr(W),Z=Tf(),te=PG(),G=L.useRef(null),ee=L.useRef(),Q=typeof c=="number"?`${c}px`:c,ie=I==="horizontal",se=ie?"width":"height",ue=L.useRef(null),ne=xm(t,ue),we=st=>Be=>{if(st){const ot=ue.current;Be===void 0?st(ot):st(ot,Be)}},de=()=>G.current?G.current[ie?"clientWidth":"clientHeight"]:0,ce=we((st,Be)=>{G.current&&ie&&(G.current.style.position="absolute"),st.style[se]=Q,m&&m(st,Be)}),ye=we((st,Be)=>{const ot=de();G.current&&ie&&(G.current.style.position="");const{duration:ct,easing:ze}=JK({style:M,timeout:O,easing:h},{mode:"enter"});if(O==="auto"){const Ke=Z.transitions.getAutoHeightDuration(ot);st.style.transitionDuration=`${Ke}ms`,ee.current=Ke}else st.style.transitionDuration=typeof ct=="string"?ct:`${ct}ms`;st.style[se]=`${ot}px`,st.style.transitionTimingFunction=ze,w&&w(st,Be)}),he=we((st,Be)=>{st.style[se]="auto",b&&b(st,Be)}),pe=we(st=>{st.style[se]=`${de()}px`,_&&_(st)}),me=we(x),be=we(st=>{const Be=de(),{duration:ot,easing:ct}=JK({style:M,timeout:O,easing:h},{mode:"exit"});if(O==="auto"){const ze=Z.transitions.getAutoHeightDuration(Be);st.style.transitionDuration=`${ze}ms`,ee.current=ze}else st.style.transitionDuration=typeof ot=="string"?ot:`${ot}ms`;st.style[se]=Q,st.style.transitionTimingFunction=ct,T&&T(st)}),xe=st=>{O==="auto"&&te.start(ee.current||0,st),r&&r(ue.current,st)},Te={slots:D,slotProps:A,component:d},[qe,et]=_o("root",{ref:ne,className:_i(U.root,l),elementType:ypr,externalForwardedProps:Te,ownerState:W,additionalProps:{style:{[ie?"minWidth":"minHeight"]:Q,...M}}}),[Ge,Me]=_o("wrapper",{ref:G,className:U.wrapper,elementType:_pr,externalForwardedProps:Te,ownerState:W}),[He,lt]=_o("wrapperInner",{className:U.wrapperInner,elementType:Cpr,externalForwardedProps:Te,ownerState:W});return k.jsx(F,{in:p,onEnter:ce,onEntered:he,onEntering:ye,onExit:pe,onExited:me,onExiting:be,addEndListener:xe,nodeRef:ue,timeout:O==="auto"?null:O,...j,children:(st,{ownerState:Be,...ot})=>{const ct={...W,state:st};return k.jsx(qe,{...et,className:_i(et.className,{entered:U.entered,exited:!p&&Q==="0px"&&U.hidden}[st]),ownerState:ct,...ot,children:k.jsx(Ge,{...Me,ownerState:ct,children:k.jsx(He,{...lt,ownerState:ct,children:o})})})}})});d6e&&(d6e.muiSupportAuto=!0);function Spr(n){return Po("MuiPaper",n)}Fo("MuiPaper",["root","rounded","outlined","elevation","elevation0","elevation1","elevation2","elevation3","elevation4","elevation5","elevation6","elevation7","elevation8","elevation9","elevation10","elevation11","elevation12","elevation13","elevation14","elevation15","elevation16","elevation17","elevation18","elevation19","elevation20","elevation21","elevation22","elevation23","elevation24"]);const xpr=n=>{const{square:e,elevation:t,variant:i,classes:r}=n,o={root:["root",i,!e&&"rounded",i==="elevation"&&`elevation${t}`]};return jo(o,Spr,r)},Epr=nn("div",{name:"MuiPaper",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.variant],!t.square&&e.rounded,t.variant==="elevation"&&e[`elevation${t.elevation}`]]}})(Gs(({theme:n})=>({backgroundColor:(n.vars||n).palette.background.paper,color:(n.vars||n).palette.text.primary,transition:n.transitions.create("box-shadow"),variants:[{props:({ownerState:e})=>!e.square,style:{borderRadius:n.shape.borderRadius}},{props:{variant:"outlined"},style:{border:`1px solid ${(n.vars||n).palette.divider}`}},{props:{variant:"elevation"},style:{boxShadow:"var(--Paper-shadow)",backgroundImage:"var(--Paper-overlay)"}}]}))),Qf=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiPaper"}),r=Tf(),{className:o,component:l="div",elevation:c=1,square:d=!1,variant:h="elevation",...p}=i,m={...i,component:l,elevation:c,square:d,variant:h},b=xpr(m);return k.jsx(Epr,{as:l,ownerState:m,className:_i(b.root,o),ref:t,...p,style:{...h==="elevation"&&{"--Paper-shadow":(r.vars||r).shadows[c],...r.vars&&{"--Paper-overlay":r.vars.overlays?.[c]},...!r.vars&&r.palette.mode==="dark"&&{"--Paper-overlay":`linear-gradient(${Wa("#fff",ROt(c))}, ${Wa("#fff",ROt(c))})`}},...p.style}})}),nui=L.createContext({});function kpr(n){return Po("MuiAccordion",n)}const RBe=Fo("MuiAccordion",["root","heading","rounded","expanded","disabled","gutters","region"]),Tpr=n=>{const{classes:e,square:t,expanded:i,disabled:r,disableGutters:o}=n;return jo({root:["root",!t&&"rounded",i&&"expanded",r&&"disabled",!o&&"gutters"],heading:["heading"],region:["region"]},kpr,e)},Lpr=nn(Qf,{name:"MuiAccordion",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[{[`& .${RBe.region}`]:e.region},e.root,!t.square&&e.rounded,!t.disableGutters&&e.gutters]}})(Gs(({theme:n})=>{const e={duration:n.transitions.duration.shortest};return{position:"relative",transition:n.transitions.create(["margin"],e),overflowAnchor:"none","&::before":{position:"absolute",left:0,top:-1,right:0,height:1,content:'""',opacity:1,backgroundColor:(n.vars||n).palette.divider,transition:n.transitions.create(["opacity","background-color"],e)},"&:first-of-type":{"&::before":{display:"none"}},[`&.${RBe.expanded}`]:{"&::before":{opacity:0},"&:first-of-type":{marginTop:0},"&:last-of-type":{marginBottom:0},"& + &":{"&::before":{display:"none"}}},[`&.${RBe.disabled}`]:{backgroundColor:(n.vars||n).palette.action.disabledBackground}}}),Gs(({theme:n})=>({variants:[{props:e=>!e.square,style:{borderRadius:0,"&:first-of-type":{borderTopLeftRadius:(n.vars||n).shape.borderRadius,borderTopRightRadius:(n.vars||n).shape.borderRadius},"&:last-of-type":{borderBottomLeftRadius:(n.vars||n).shape.borderRadius,borderBottomRightRadius:(n.vars||n).shape.borderRadius,"@supports (-ms-ime-align: auto)":{borderBottomLeftRadius:0,borderBottomRightRadius:0}}}},{props:e=>!e.disableGutters,style:{[`&.${RBe.expanded}`]:{margin:"16px 0"}}}]}))),Dpr=nn("h3",{name:"MuiAccordion",slot:"Heading"})({all:"unset"}),Ipr=nn("div",{name:"MuiAccordion",slot:"Region"})({}),Kzt=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiAccordion"}),{children:r,className:o,defaultExpanded:l=!1,disabled:c=!1,disableGutters:d=!1,expanded:h,onChange:p,slots:m={},slotProps:b={},TransitionComponent:w,TransitionProps:_,...x}=i,[T,I]=E9({controlled:h,default:l,name:"Accordion",state:"expanded"}),D=L.useCallback(we=>{I(!T),p&&p(we,!T)},[T,p,I]),[A,...M]=L.Children.toArray(r),O=L.useMemo(()=>({expanded:T,disabled:c,disableGutters:d,toggle:D}),[T,c,d,D]),F={...i,disabled:c,disableGutters:d,expanded:T},j=Tpr(F),W={transition:w,...m},U={transition:_,...b},Z={slots:W,slotProps:U},[te,G]=_o("root",{elementType:Lpr,externalForwardedProps:{...Z,...x},className:_i(j.root,o),shouldForwardComponentProp:!0,ownerState:F,ref:t}),[ee,Q]=_o("heading",{elementType:Dpr,externalForwardedProps:Z,className:j.heading,ownerState:F}),[ie,se]=_o("transition",{elementType:d6e,externalForwardedProps:Z,ownerState:F}),[ue,ne]=_o("region",{elementType:Ipr,externalForwardedProps:Z,ownerState:F,className:j.region,additionalProps:{"aria-labelledby":A.props.id,id:A.props["aria-controls"],role:"region"}});return k.jsxs(te,{...G,children:[k.jsx(ee,{...Q,children:k.jsx(nui.Provider,{value:O,children:A})}),k.jsx(ie,{in:T,timeout:"auto",...se,children:k.jsx(ue,{...ne,children:M})})]})});function Apr(n){return Po("MuiAccordionDetails",n)}Fo("MuiAccordionDetails",["root"]);const Rpr=n=>{const{classes:e}=n;return jo({root:["root"]},Apr,e)},Mpr=nn("div",{name:"MuiAccordionDetails",slot:"Root"})(Gs(({theme:n})=>({padding:n.spacing(1,2,2)}))),Yzt=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiAccordionDetails"}),{className:r,...o}=i,l=i,c=Rpr(l);return k.jsx(Mpr,{className:_i(c.root,r),ref:t,ownerState:l,...o})});function eY(n){try{return n.matches(":focus-visible")}catch{}return!1}class rGe{static create(){return new rGe}static use(){const e=Qci(rGe.create).current,[t,i]=L.useState(!1);return e.shouldMount=t,e.setShouldMount=i,L.useEffect(e.mountEffect,[t]),e}constructor(){this.ref={current:null},this.mounted=null,this.didMount=!1,this.shouldMount=!1,this.setShouldMount=null}mount(){return this.mounted||(this.mounted=Npr(),this.shouldMount=!0,this.setShouldMount(this.shouldMount)),this.mounted}mountEffect=()=>{this.shouldMount&&!this.didMount&&this.ref.current!==null&&(this.didMount=!0,this.mounted.resolve())};start(...e){this.mount().then(()=>this.ref.current?.start(...e))}stop(...e){this.mount().then(()=>this.ref.current?.stop(...e))}pulsate(...e){this.mount().then(()=>this.ref.current?.pulsate(...e))}}function Opr(){return rGe.use()}function Npr(){let n,e;const t=new Promise((i,r)=>{n=i,e=r});return t.resolve=n,t.reject=e,t}function Ppr(n){const{className:e,classes:t,pulsate:i=!1,rippleX:r,rippleY:o,rippleSize:l,in:c,onExited:d,timeout:h}=n,[p,m]=L.useState(!1),b=_i(e,t.ripple,t.rippleVisible,i&&t.ripplePulsate),w={width:l,height:l,top:-(l/2)+o,left:-(l/2)+r},_=_i(t.child,p&&t.childLeaving,i&&t.childPulsate);return!c&&!p&&m(!0),L.useEffect(()=>{if(!c&&d!=null){const x=setTimeout(d,h);return()=>{clearTimeout(x)}}},[d,c,h]),k.jsx("span",{className:b,style:w,children:k.jsx("span",{className:_})})}const XD=Fo("MuiTouchRipple",["root","ripple","rippleVisible","ripplePulsate","child","childLeaving","childPulsate"]),qOt=550,Fpr=80,jpr=W3` 0% { transform: scale(0); opacity: 0.1; @@ -54,7 +54,7 @@ Error generating stack: `+je.message+` transform: scale(1); opacity: 0.3; } -`,Mpr=W3` +`,Hpr=W3` 0% { opacity: 1; } @@ -62,7 +62,7 @@ Error generating stack: `+je.message+` 100% { opacity: 0; } -`,Opr=W3` +`,Bpr=W3` 0% { transform: scale(1); } @@ -74,23 +74,23 @@ Error generating stack: `+je.message+` 100% { transform: scale(1); } -`,Npr=tn("span",{name:"MuiTouchRipple",slot:"Root"})({overflow:"hidden",pointerEvents:"none",position:"absolute",zIndex:0,top:0,right:0,bottom:0,left:0,borderRadius:"inherit"}),Ppr=tn(Ipr,{name:"MuiTouchRipple",slot:"Ripple"})` +`,Wpr=nn("span",{name:"MuiTouchRipple",slot:"Root"})({overflow:"hidden",pointerEvents:"none",position:"absolute",zIndex:0,top:0,right:0,bottom:0,left:0,borderRadius:"inherit"}),Vpr=nn(Ppr,{name:"MuiTouchRipple",slot:"Ripple"})` opacity: 0; position: absolute; - &.${QD.rippleVisible} { + &.${XD.rippleVisible} { opacity: 0.3; transform: scale(1); - animation-name: ${Rpr}; - animation-duration: ${GOt}ms; + animation-name: ${jpr}; + animation-duration: ${qOt}ms; animation-timing-function: ${({theme:n})=>n.transitions.easing.easeInOut}; } - &.${QD.ripplePulsate} { + &.${XD.ripplePulsate} { animation-duration: ${({theme:n})=>n.transitions.duration.shorter}ms; } - & .${QD.child} { + & .${XD.child} { opacity: 1; display: block; width: 100%; @@ -99,25 +99,25 @@ Error generating stack: `+je.message+` background-color: currentColor; } - & .${QD.childLeaving} { + & .${XD.childLeaving} { opacity: 0; - animation-name: ${Mpr}; - animation-duration: ${GOt}ms; + animation-name: ${Hpr}; + animation-duration: ${qOt}ms; animation-timing-function: ${({theme:n})=>n.transitions.easing.easeInOut}; } - & .${QD.childPulsate} { + & .${XD.childPulsate} { position: absolute; /* @noflip */ left: 0px; top: 0; - animation-name: ${Opr}; + animation-name: ${Bpr}; animation-duration: 2500ms; animation-timing-function: ${({theme:n})=>n.transitions.easing.easeInOut}; animation-iteration-count: infinite; animation-delay: 200ms; } -`,Fpr=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiTouchRipple"}),{center:r=!1,classes:o={},className:l,...c}=i,[d,h]=D.useState([]),p=D.useRef(0),m=D.useRef(null);D.useEffect(()=>{m.current&&(m.current(),m.current=null)},[d]);const b=D.useRef(!1),w=NG(),_=D.useRef(null),x=D.useRef(null),T=D.useCallback(M=>{const{pulsate:O,rippleX:F,rippleY:j,rippleSize:W,cb:q}=M;h(Z=>[...Z,k.jsx(Ppr,{classes:{ripple:_i(o.ripple,QD.ripple),rippleVisible:_i(o.rippleVisible,QD.rippleVisible),ripplePulsate:_i(o.ripplePulsate,QD.ripplePulsate),child:_i(o.child,QD.child),childLeaving:_i(o.childLeaving,QD.childLeaving),childPulsate:_i(o.childPulsate,QD.childPulsate)},timeout:GOt,pulsate:O,rippleX:F,rippleY:j,rippleSize:W},p.current)]),p.current+=1,m.current=q},[o]),I=D.useCallback((M={},O={},F=()=>{})=>{const{pulsate:j=!1,center:W=r||O.pulsate,fakeElement:q=!1}=O;if(M?.type==="mousedown"&&b.current){b.current=!1;return}M?.type==="touchstart"&&(b.current=!0);const Z=q?null:x.current,ee=Z?Z.getBoundingClientRect():{width:0,height:0,left:0,top:0};let G,te,Q;if(W||M===void 0||M.clientX===0&&M.clientY===0||!M.clientX&&!M.touches)G=Math.round(ee.width/2),te=Math.round(ee.height/2);else{const{clientX:ie,clientY:se}=M.touches&&M.touches.length>0?M.touches[0]:M;G=Math.round(ie-ee.left),te=Math.round(se-ee.top)}if(W)Q=Math.sqrt((2*ee.width**2+ee.height**2)/3),Q%2===0&&(Q+=1);else{const ie=Math.max(Math.abs((Z?Z.clientWidth:0)-G),G)*2+2,se=Math.max(Math.abs((Z?Z.clientHeight:0)-te),te)*2+2;Q=Math.sqrt(ie**2+se**2)}M?.touches?_.current===null&&(_.current=()=>{T({pulsate:j,rippleX:G,rippleY:te,rippleSize:Q,cb:F})},w.start(Apr,()=>{_.current&&(_.current(),_.current=null)})):T({pulsate:j,rippleX:G,rippleY:te,rippleSize:Q,cb:F})},[r,T,w]),L=D.useCallback(()=>{I({},{pulsate:!0})},[I]),A=D.useCallback((M,O)=>{if(w.clear(),M?.type==="touchend"&&_.current){_.current(),_.current=null,w.start(0,()=>{A(M,O)});return}_.current=null,h(F=>F.length>0?F.slice(1):F),m.current=O},[w]);return D.useImperativeHandle(t,()=>({pulsate:L,start:I,stop:A}),[L,I,A]),k.jsx(Npr,{className:_i(QD.root,o.root,l),ref:x,...c,children:k.jsx(m5e,{component:null,exit:!0,children:d})})});function jpr(n){return No("MuiButtonBase",n)}const Hpr=Po("MuiButtonBase",["root","disabled","focusVisible"]),Bpr=n=>{const{disabled:e,focusVisible:t,focusVisibleClassName:i,classes:r}=n,l=Fo({root:["root",e&&"disabled",t&&"focusVisible"]},jpr,r);return t&&i&&(l.root+=` ${i}`),l},Wpr=tn("button",{name:"MuiButtonBase",slot:"Root"})({display:"inline-flex",alignItems:"center",justifyContent:"center",position:"relative",boxSizing:"border-box",WebkitTapHighlightColor:"transparent",backgroundColor:"transparent",outline:0,border:0,margin:0,borderRadius:0,padding:0,cursor:"pointer",userSelect:"none",verticalAlign:"middle",MozAppearance:"none",WebkitAppearance:"none",textDecoration:"none",color:"inherit","&::-moz-focus-inner":{borderStyle:"none"},[`&.${Hpr.disabled}`]:{pointerEvents:"none",cursor:"default"},"@media print":{colorAdjust:"exact"}}),D3=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiButtonBase"}),{action:r,centerRipple:o=!1,children:l,className:c,component:d="button",disabled:h=!1,disableRipple:p=!1,disableTouchRipple:m=!1,focusRipple:b=!1,focusVisibleClassName:w,LinkComponent:_="a",onBlur:x,onClick:T,onContextMenu:I,onDragLeave:L,onFocus:A,onFocusVisible:M,onKeyDown:O,onKeyUp:F,onMouseDown:j,onMouseLeave:W,onMouseUp:q,onTouchEnd:Z,onTouchMove:ee,onTouchStart:G,tabIndex:te=0,TouchRippleProps:Q,touchRippleRef:ie,type:se,...de}=i,ne=D.useRef(null),we=Lpr(),ue=xm(we.ref,ie),[ce,ye]=D.useState(!1);h&&ce&&ye(!1),D.useImperativeHandle(r,()=>({focusVisible:()=>{ye(!0),ne.current.focus()}}),[]);const he=we.shouldMount&&!p&&!h;D.useEffect(()=>{ce&&b&&!p&&we.pulsate()},[p,b,ce,we]);const pe=kH(we,"start",j,m),me=kH(we,"stop",I,m),be=kH(we,"stop",L,m),xe=kH(we,"stop",q,m),Te=kH(we,"stop",nt=>{ce&&nt.preventDefault(),W&&W(nt)},m),Ge=kH(we,"start",G,m),tt=kH(we,"stop",Z,m),Ue=kH(we,"stop",ee,m),Me=kH(we,"stop",nt=>{JK(nt.target)||ye(!1),x&&x(nt)},!1),He=ub(nt=>{ne.current||(ne.current=nt.currentTarget),JK(nt.target)&&(ye(!0),M&&M(nt)),A&&A(nt)}),at=()=>{const nt=ne.current;return d&&d!=="button"&&!(nt.tagName==="A"&&nt.href)},rt=ub(nt=>{b&&!nt.repeat&&ce&&nt.key===" "&&we.stop(nt,()=>{we.start(nt)}),nt.target===nt.currentTarget&&at()&&nt.key===" "&&nt.preventDefault(),O&&O(nt),nt.target===nt.currentTarget&&at()&&nt.key==="Enter"&&!h&&(nt.preventDefault(),T&&T(nt))}),Be=ub(nt=>{b&&nt.key===" "&&ce&&!nt.defaultPrevented&&we.stop(nt,()=>{we.pulsate(nt)}),F&&F(nt),T&&nt.target===nt.currentTarget&&at()&&nt.key===" "&&!nt.defaultPrevented&&T(nt)});let lt=d;lt==="button"&&(de.href||de.to)&&(lt=_);const ct={};if(lt==="button"){const nt=!!de.formAction;ct.type=se===void 0&&!nt?"button":se,ct.disabled=h}else!de.href&&!de.to&&(ct.role="button"),h&&(ct["aria-disabled"]=h);const ze=xm(t,ne),Ke={...i,centerRipple:o,component:d,disabled:h,disableRipple:p,disableTouchRipple:m,focusRipple:b,tabIndex:te,focusVisible:ce},$e=Bpr(Ke);return k.jsxs(Wpr,{as:lt,className:_i($e.root,c),ownerState:Ke,onBlur:Me,onClick:T,onContextMenu:me,onFocus:He,onKeyDown:rt,onKeyUp:Be,onMouseDown:pe,onMouseLeave:Te,onMouseUp:xe,onDragLeave:be,onTouchEnd:tt,onTouchMove:Ue,onTouchStart:Ge,ref:ze,tabIndex:h?-1:te,type:se,...ct,...de,children:[l,he?k.jsx(Fpr,{ref:ue,center:o,...Q}):null]})});function kH(n,e,t,i=!1){return ub(r=>(t&&t(r),i||n[e](r),!0))}function Vpr(n){return No("MuiAccordionSummary",n)}const jfe=Po("MuiAccordionSummary",["root","expanded","focusVisible","disabled","gutters","contentGutters","content","expandIconWrapper"]),$pr=n=>{const{classes:e,expanded:t,disabled:i,disableGutters:r}=n;return Fo({root:["root",t&&"expanded",i&&"disabled",!r&&"gutters"],focusVisible:["focusVisible"],content:["content",t&&"expanded",!r&&"contentGutters"],expandIconWrapper:["expandIconWrapper",t&&"expanded"]},Vpr,e)},zpr=tn(D3,{name:"MuiAccordionSummary",slot:"Root"})(Gs(({theme:n})=>{const e={duration:n.transitions.duration.shortest};return{display:"flex",width:"100%",minHeight:48,padding:n.spacing(0,2),transition:n.transitions.create(["min-height","background-color"],e),[`&.${jfe.focusVisible}`]:{backgroundColor:(n.vars||n).palette.action.focus},[`&.${jfe.disabled}`]:{opacity:(n.vars||n).palette.action.disabledOpacity},[`&:hover:not(.${jfe.disabled})`]:{cursor:"pointer"},variants:[{props:t=>!t.disableGutters,style:{[`&.${jfe.expanded}`]:{minHeight:64}}}]}})),Upr=tn("span",{name:"MuiAccordionSummary",slot:"Content"})(Gs(({theme:n})=>({display:"flex",textAlign:"start",flexGrow:1,margin:"12px 0",variants:[{props:e=>!e.disableGutters,style:{transition:n.transitions.create(["margin"],{duration:n.transitions.duration.shortest}),[`&.${jfe.expanded}`]:{margin:"20px 0"}}}]}))),qpr=tn("span",{name:"MuiAccordionSummary",slot:"ExpandIconWrapper"})(Gs(({theme:n})=>({display:"flex",color:(n.vars||n).palette.action.active,transform:"rotate(0deg)",transition:n.transitions.create("transform",{duration:n.transitions.duration.shortest}),[`&.${jfe.expanded}`]:{transform:"rotate(180deg)"}}))),Qzt=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiAccordionSummary"}),{children:r,className:o,expandIcon:l,focusVisibleClassName:c,onClick:d,slots:h,slotProps:p,...m}=i,{disabled:b=!1,disableGutters:w,expanded:_,toggle:x}=D.useContext(Jci),T=Z=>{x&&x(Z),d&&d(Z)},I={...i,expanded:_,disabled:b,disableGutters:w},L=$pr(I),A={slots:h,slotProps:p},[M,O]=_o("root",{ref:t,shouldForwardComponentProp:!0,className:_i(L.root,o),elementType:zpr,externalForwardedProps:{...A,...m},ownerState:I,additionalProps:{focusRipple:!1,disableRipple:!0,disabled:b,"aria-expanded":_,focusVisibleClassName:_i(L.focusVisible,c)},getSlotProps:Z=>({...Z,onClick:ee=>{Z.onClick?.(ee),T(ee)}})}),[F,j]=_o("content",{className:L.content,elementType:Upr,externalForwardedProps:A,ownerState:I}),[W,q]=_o("expandIconWrapper",{className:L.expandIconWrapper,elementType:qpr,externalForwardedProps:A,ownerState:I});return k.jsxs(M,{...O,children:[k.jsx(F,{...j,children:r}),l&&k.jsx(W,{...q,children:l})]})});function Gpr(n){return typeof n.main=="string"}function Kpr(n,e=[]){if(!Gpr(n))return!1;for(const t of e)if(!n.hasOwnProperty(t)||typeof n[t]!="string")return!1;return!0}function Vh(n=[]){return([,e])=>e&&Kpr(e,n)}function Ypr(n){return No("MuiAlert",n)}const uPn=Po("MuiAlert",["root","action","icon","message","filled","colorSuccess","colorInfo","colorWarning","colorError","filledSuccess","filledInfo","filledWarning","filledError","outlined","outlinedSuccess","outlinedInfo","outlinedWarning","outlinedError","standard","standardSuccess","standardInfo","standardWarning","standardError"]);function Zpr(n){return No("MuiCircularProgress",n)}Po("MuiCircularProgress",["root","determinate","indeterminate","colorPrimary","colorSecondary","svg","track","circle","circleDeterminate","circleIndeterminate","circleDisableShrink"]);const vR=44,KOt=W3` +`,$pr=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiTouchRipple"}),{center:r=!1,classes:o={},className:l,...c}=i,[d,h]=L.useState([]),p=L.useRef(0),m=L.useRef(null);L.useEffect(()=>{m.current&&(m.current(),m.current=null)},[d]);const b=L.useRef(!1),w=PG(),_=L.useRef(null),x=L.useRef(null),T=L.useCallback(M=>{const{pulsate:O,rippleX:F,rippleY:j,rippleSize:W,cb:U}=M;h(Z=>[...Z,k.jsx(Vpr,{classes:{ripple:_i(o.ripple,XD.ripple),rippleVisible:_i(o.rippleVisible,XD.rippleVisible),ripplePulsate:_i(o.ripplePulsate,XD.ripplePulsate),child:_i(o.child,XD.child),childLeaving:_i(o.childLeaving,XD.childLeaving),childPulsate:_i(o.childPulsate,XD.childPulsate)},timeout:qOt,pulsate:O,rippleX:F,rippleY:j,rippleSize:W},p.current)]),p.current+=1,m.current=U},[o]),I=L.useCallback((M={},O={},F=()=>{})=>{const{pulsate:j=!1,center:W=r||O.pulsate,fakeElement:U=!1}=O;if(M?.type==="mousedown"&&b.current){b.current=!1;return}M?.type==="touchstart"&&(b.current=!0);const Z=U?null:x.current,te=Z?Z.getBoundingClientRect():{width:0,height:0,left:0,top:0};let G,ee,Q;if(W||M===void 0||M.clientX===0&&M.clientY===0||!M.clientX&&!M.touches)G=Math.round(te.width/2),ee=Math.round(te.height/2);else{const{clientX:ie,clientY:se}=M.touches&&M.touches.length>0?M.touches[0]:M;G=Math.round(ie-te.left),ee=Math.round(se-te.top)}if(W)Q=Math.sqrt((2*te.width**2+te.height**2)/3),Q%2===0&&(Q+=1);else{const ie=Math.max(Math.abs((Z?Z.clientWidth:0)-G),G)*2+2,se=Math.max(Math.abs((Z?Z.clientHeight:0)-ee),ee)*2+2;Q=Math.sqrt(ie**2+se**2)}M?.touches?_.current===null&&(_.current=()=>{T({pulsate:j,rippleX:G,rippleY:ee,rippleSize:Q,cb:F})},w.start(Fpr,()=>{_.current&&(_.current(),_.current=null)})):T({pulsate:j,rippleX:G,rippleY:ee,rippleSize:Q,cb:F})},[r,T,w]),D=L.useCallback(()=>{I({},{pulsate:!0})},[I]),A=L.useCallback((M,O)=>{if(w.clear(),M?.type==="touchend"&&_.current){_.current(),_.current=null,w.start(0,()=>{A(M,O)});return}_.current=null,h(F=>F.length>0?F.slice(1):F),m.current=O},[w]);return L.useImperativeHandle(t,()=>({pulsate:D,start:I,stop:A}),[D,I,A]),k.jsx(Wpr,{className:_i(XD.root,o.root,l),ref:x,...c,children:k.jsx(g5e,{component:null,exit:!0,children:d})})});function zpr(n){return Po("MuiButtonBase",n)}const Upr=Fo("MuiButtonBase",["root","disabled","focusVisible"]),qpr=n=>{const{disabled:e,focusVisible:t,focusVisibleClassName:i,classes:r}=n,l=jo({root:["root",e&&"disabled",t&&"focusVisible"]},zpr,r);return t&&i&&(l.root+=` ${i}`),l},Gpr=nn("button",{name:"MuiButtonBase",slot:"Root"})({display:"inline-flex",alignItems:"center",justifyContent:"center",position:"relative",boxSizing:"border-box",WebkitTapHighlightColor:"transparent",backgroundColor:"transparent",outline:0,border:0,margin:0,borderRadius:0,padding:0,cursor:"pointer",userSelect:"none",verticalAlign:"middle",MozAppearance:"none",WebkitAppearance:"none",textDecoration:"none",color:"inherit","&::-moz-focus-inner":{borderStyle:"none"},[`&.${Upr.disabled}`]:{pointerEvents:"none",cursor:"default"},"@media print":{colorAdjust:"exact"}}),D3=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiButtonBase"}),{action:r,centerRipple:o=!1,children:l,className:c,component:d="button",disabled:h=!1,disableRipple:p=!1,disableTouchRipple:m=!1,focusRipple:b=!1,focusVisibleClassName:w,LinkComponent:_="a",onBlur:x,onClick:T,onContextMenu:I,onDragLeave:D,onFocus:A,onFocusVisible:M,onKeyDown:O,onKeyUp:F,onMouseDown:j,onMouseLeave:W,onMouseUp:U,onTouchEnd:Z,onTouchMove:te,onTouchStart:G,tabIndex:ee=0,TouchRippleProps:Q,touchRippleRef:ie,type:se,...ue}=i,ne=L.useRef(null),we=Opr(),de=xm(we.ref,ie),[ce,ye]=L.useState(!1);h&&ce&&ye(!1),L.useImperativeHandle(r,()=>({focusVisible:()=>{ye(!0),ne.current.focus()}}),[]);const he=we.shouldMount&&!p&&!h;L.useEffect(()=>{ce&&b&&!p&&we.pulsate()},[p,b,ce,we]);const pe=EH(we,"start",j,m),me=EH(we,"stop",I,m),be=EH(we,"stop",D,m),xe=EH(we,"stop",U,m),Te=EH(we,"stop",tt=>{ce&&tt.preventDefault(),W&&W(tt)},m),qe=EH(we,"start",G,m),et=EH(we,"stop",Z,m),Ge=EH(we,"stop",te,m),Me=EH(we,"stop",tt=>{eY(tt.target)||ye(!1),x&&x(tt)},!1),He=db(tt=>{ne.current||(ne.current=tt.currentTarget),eY(tt.target)&&(ye(!0),M&&M(tt)),A&&A(tt)}),lt=()=>{const tt=ne.current;return d&&d!=="button"&&!(tt.tagName==="A"&&tt.href)},st=db(tt=>{b&&!tt.repeat&&ce&&tt.key===" "&&we.stop(tt,()=>{we.start(tt)}),tt.target===tt.currentTarget&<()&&tt.key===" "&&tt.preventDefault(),O&&O(tt),tt.target===tt.currentTarget&<()&&tt.key==="Enter"&&!h&&(tt.preventDefault(),T&&T(tt))}),Be=db(tt=>{b&&tt.key===" "&&ce&&!tt.defaultPrevented&&we.stop(tt,()=>{we.pulsate(tt)}),F&&F(tt),T&&tt.target===tt.currentTarget&<()&&tt.key===" "&&!tt.defaultPrevented&&T(tt)});let ot=d;ot==="button"&&(ue.href||ue.to)&&(ot=_);const ct={};if(ot==="button"){const tt=!!ue.formAction;ct.type=se===void 0&&!tt?"button":se,ct.disabled=h}else!ue.href&&!ue.to&&(ct.role="button"),h&&(ct["aria-disabled"]=h);const ze=xm(t,ne),Ke={...i,centerRipple:o,component:d,disabled:h,disableRipple:p,disableTouchRipple:m,focusRipple:b,tabIndex:ee,focusVisible:ce},$e=qpr(Ke);return k.jsxs(Gpr,{as:ot,className:_i($e.root,c),ownerState:Ke,onBlur:Me,onClick:T,onContextMenu:me,onFocus:He,onKeyDown:st,onKeyUp:Be,onMouseDown:pe,onMouseLeave:Te,onMouseUp:xe,onDragLeave:be,onTouchEnd:et,onTouchMove:Ge,onTouchStart:qe,ref:ze,tabIndex:h?-1:ee,type:se,...ct,...ue,children:[l,he?k.jsx($pr,{ref:de,center:o,...Q}):null]})});function EH(n,e,t,i=!1){return db(r=>(t&&t(r),i||n[e](r),!0))}function Kpr(n){return Po("MuiAccordionSummary",n)}const Nfe=Fo("MuiAccordionSummary",["root","expanded","focusVisible","disabled","gutters","contentGutters","content","expandIconWrapper"]),Ypr=n=>{const{classes:e,expanded:t,disabled:i,disableGutters:r}=n;return jo({root:["root",t&&"expanded",i&&"disabled",!r&&"gutters"],focusVisible:["focusVisible"],content:["content",t&&"expanded",!r&&"contentGutters"],expandIconWrapper:["expandIconWrapper",t&&"expanded"]},Kpr,e)},Zpr=nn(D3,{name:"MuiAccordionSummary",slot:"Root"})(Gs(({theme:n})=>{const e={duration:n.transitions.duration.shortest};return{display:"flex",width:"100%",minHeight:48,padding:n.spacing(0,2),transition:n.transitions.create(["min-height","background-color"],e),[`&.${Nfe.focusVisible}`]:{backgroundColor:(n.vars||n).palette.action.focus},[`&.${Nfe.disabled}`]:{opacity:(n.vars||n).palette.action.disabledOpacity},[`&:hover:not(.${Nfe.disabled})`]:{cursor:"pointer"},variants:[{props:t=>!t.disableGutters,style:{[`&.${Nfe.expanded}`]:{minHeight:64}}}]}})),Xpr=nn("span",{name:"MuiAccordionSummary",slot:"Content"})(Gs(({theme:n})=>({display:"flex",textAlign:"start",flexGrow:1,margin:"12px 0",variants:[{props:e=>!e.disableGutters,style:{transition:n.transitions.create(["margin"],{duration:n.transitions.duration.shortest}),[`&.${Nfe.expanded}`]:{margin:"20px 0"}}}]}))),Qpr=nn("span",{name:"MuiAccordionSummary",slot:"ExpandIconWrapper"})(Gs(({theme:n})=>({display:"flex",color:(n.vars||n).palette.action.active,transform:"rotate(0deg)",transition:n.transitions.create("transform",{duration:n.transitions.duration.shortest}),[`&.${Nfe.expanded}`]:{transform:"rotate(180deg)"}}))),Zzt=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiAccordionSummary"}),{children:r,className:o,expandIcon:l,focusVisibleClassName:c,onClick:d,slots:h,slotProps:p,...m}=i,{disabled:b=!1,disableGutters:w,expanded:_,toggle:x}=L.useContext(nui),T=Z=>{x&&x(Z),d&&d(Z)},I={...i,expanded:_,disabled:b,disableGutters:w},D=Ypr(I),A={slots:h,slotProps:p},[M,O]=_o("root",{ref:t,shouldForwardComponentProp:!0,className:_i(D.root,o),elementType:Zpr,externalForwardedProps:{...A,...m},ownerState:I,additionalProps:{focusRipple:!1,disableRipple:!0,disabled:b,"aria-expanded":_,focusVisibleClassName:_i(D.focusVisible,c)},getSlotProps:Z=>({...Z,onClick:te=>{Z.onClick?.(te),T(te)}})}),[F,j]=_o("content",{className:D.content,elementType:Xpr,externalForwardedProps:A,ownerState:I}),[W,U]=_o("expandIconWrapper",{className:D.expandIconWrapper,elementType:Qpr,externalForwardedProps:A,ownerState:I});return k.jsxs(M,{...O,children:[k.jsx(F,{...j,children:r}),l&&k.jsx(W,{...U,children:l})]})});function Jpr(n){return typeof n.main=="string"}function egr(n,e=[]){if(!Jpr(n))return!1;for(const t of e)if(!n.hasOwnProperty(t)||typeof n[t]!="string")return!1;return!0}function Vh(n=[]){return([,e])=>e&&egr(e,n)}function tgr(n){return Po("MuiAlert",n)}const dPn=Fo("MuiAlert",["root","action","icon","message","filled","colorSuccess","colorInfo","colorWarning","colorError","filledSuccess","filledInfo","filledWarning","filledError","outlined","outlinedSuccess","outlinedInfo","outlinedWarning","outlinedError","standard","standardSuccess","standardInfo","standardWarning","standardError"]);function ngr(n){return Po("MuiCircularProgress",n)}Fo("MuiCircularProgress",["root","determinate","indeterminate","colorPrimary","colorSecondary","svg","track","circle","circleDeterminate","circleIndeterminate","circleDisableShrink"]);const vR=44,GOt=W3` 0% { transform: rotate(0deg); } @@ -125,7 +125,7 @@ Error generating stack: `+je.message+` 100% { transform: rotate(360deg); } -`,YOt=W3` +`,KOt=W3` 0% { stroke-dasharray: 1px, 200px; stroke-dashoffset: 0; @@ -140,13 +140,13 @@ Error generating stack: `+je.message+` stroke-dasharray: 1px, 200px; stroke-dashoffset: -126px; } -`,Xpr=typeof KOt!="string"?U1e` - animation: ${KOt} 1.4s linear infinite; - `:null,Qpr=typeof YOt!="string"?U1e` - animation: ${YOt} 1.4s ease-in-out infinite; - `:null,Jpr=n=>{const{classes:e,variant:t,color:i,disableShrink:r}=n,o={root:["root",t,`color${ii(i)}`],svg:["svg"],track:["track"],circle:["circle",`circle${ii(t)}`,r&&"circleDisableShrink"]};return Fo(o,Zpr,e)},egr=tn("span",{name:"MuiCircularProgress",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.variant],e[`color${ii(t.color)}`]]}})(Gs(({theme:n})=>({display:"inline-block",variants:[{props:{variant:"determinate"},style:{transition:n.transitions.create("transform")}},{props:{variant:"indeterminate"},style:Xpr||{animation:`${KOt} 1.4s linear infinite`}},...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{color:(n.vars||n).palette[e].main}}))]}))),tgr=tn("svg",{name:"MuiCircularProgress",slot:"Svg"})({display:"block"}),ngr=tn("circle",{name:"MuiCircularProgress",slot:"Circle",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.circle,e[`circle${ii(t.variant)}`],t.disableShrink&&e.circleDisableShrink]}})(Gs(({theme:n})=>({stroke:"currentColor",variants:[{props:{variant:"determinate"},style:{transition:n.transitions.create("stroke-dashoffset")}},{props:{variant:"indeterminate"},style:{strokeDasharray:"80px, 200px",strokeDashoffset:0}},{props:({ownerState:e})=>e.variant==="indeterminate"&&!e.disableShrink,style:Qpr||{animation:`${YOt} 1.4s ease-in-out infinite`}}]}))),igr=tn("circle",{name:"MuiCircularProgress",slot:"Track"})(Gs(({theme:n})=>({stroke:"currentColor",opacity:(n.vars||n).palette.action.activatedOpacity}))),fv=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiCircularProgress"}),{className:r,color:o="primary",disableShrink:l=!1,enableTrackSlot:c=!1,size:d=40,style:h,thickness:p=3.6,value:m=0,variant:b="indeterminate",...w}=i,_={...i,color:o,disableShrink:l,size:d,thickness:p,value:m,variant:b,enableTrackSlot:c},x=Jpr(_),T={},I={},L={};if(b==="determinate"){const A=2*Math.PI*((vR-p)/2);T.strokeDasharray=A.toFixed(3),L["aria-valuenow"]=Math.round(m),T.strokeDashoffset=`${((100-m)/100*A).toFixed(3)}px`,I.transform="rotate(-90deg)"}return k.jsx(egr,{className:_i(x.root,r),style:{width:d,height:d,...I,...h},ownerState:_,ref:t,role:"progressbar",...L,...w,children:k.jsxs(tgr,{className:x.svg,ownerState:_,viewBox:`${vR/2} ${vR/2} ${vR} ${vR}`,children:[c?k.jsx(igr,{className:x.track,ownerState:_,cx:vR,cy:vR,r:(vR-p)/2,fill:"none",strokeWidth:p,"aria-hidden":"true"}):null,k.jsx(ngr,{className:x.circle,style:T,ownerState:_,cx:vR,cy:vR,r:(vR-p)/2,fill:"none",strokeWidth:p})]})})});function rgr(n){return No("MuiIconButton",n)}const dPn=Po("MuiIconButton",["root","disabled","colorInherit","colorPrimary","colorSecondary","colorError","colorInfo","colorSuccess","colorWarning","edgeStart","edgeEnd","sizeSmall","sizeMedium","sizeLarge","loading","loadingIndicator","loadingWrapper"]),sgr=n=>{const{classes:e,disabled:t,color:i,edge:r,size:o,loading:l}=n,c={root:["root",l&&"loading",t&&"disabled",i!=="default"&&`color${ii(i)}`,r&&`edge${ii(r)}`,`size${ii(o)}`],loadingIndicator:["loadingIndicator"],loadingWrapper:["loadingWrapper"]};return Fo(c,rgr,e)},ogr=tn(D3,{name:"MuiIconButton",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.loading&&e.loading,t.color!=="default"&&e[`color${ii(t.color)}`],t.edge&&e[`edge${ii(t.edge)}`],e[`size${ii(t.size)}`]]}})(Gs(({theme:n})=>({textAlign:"center",flex:"0 0 auto",fontSize:n.typography.pxToRem(24),padding:8,borderRadius:"50%",color:(n.vars||n).palette.action.active,transition:n.transitions.create("background-color",{duration:n.transitions.duration.shortest}),variants:[{props:e=>!e.disableRipple,style:{"--IconButton-hoverBg":n.alpha((n.vars||n).palette.action.active,(n.vars||n).palette.action.hoverOpacity),"&:hover":{backgroundColor:"var(--IconButton-hoverBg)","@media (hover: none)":{backgroundColor:"transparent"}}}},{props:{edge:"start"},style:{marginLeft:-12}},{props:{edge:"start",size:"small"},style:{marginLeft:-3}},{props:{edge:"end"},style:{marginRight:-12}},{props:{edge:"end",size:"small"},style:{marginRight:-3}}]})),Gs(({theme:n})=>({variants:[{props:{color:"inherit"},style:{color:"inherit"}},...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{color:(n.vars||n).palette[e].main}})),...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{"--IconButton-hoverBg":n.alpha((n.vars||n).palette[e].main,(n.vars||n).palette.action.hoverOpacity)}})),{props:{size:"small"},style:{padding:5,fontSize:n.typography.pxToRem(18)}},{props:{size:"large"},style:{padding:12,fontSize:n.typography.pxToRem(28)}}],[`&.${dPn.disabled}`]:{backgroundColor:"transparent",color:(n.vars||n).palette.action.disabled},[`&.${dPn.loading}`]:{color:"transparent"}}))),agr=tn("span",{name:"MuiIconButton",slot:"LoadingIndicator"})(({theme:n})=>({display:"none",position:"absolute",visibility:"visible",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:(n.vars||n).palette.action.disabled,variants:[{props:{loading:!0},style:{display:"flex"}}]})),da=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiIconButton"}),{edge:r=!1,children:o,className:l,color:c="default",disabled:d=!1,disableFocusRipple:h=!1,size:p="medium",id:m,loading:b=null,loadingIndicator:w,..._}=i,x=YW(m),T=w??k.jsx(fv,{"aria-labelledby":x,color:"inherit",size:16}),I={...i,edge:r,color:c,disabled:d,disableFocusRipple:h,loading:b,loadingIndicator:T,size:p},L=sgr(I);return k.jsxs(ogr,{id:b?x:m,className:_i(L.root,l),centerRipple:!0,focusRipple:!h,disabled:d||b,ref:t,..._,ownerState:I,children:[typeof b=="boolean"&&k.jsx("span",{className:L.loadingWrapper,style:{display:"contents"},children:k.jsx(agr,{className:L.loadingIndicator,ownerState:I,children:b&&T})}),o]})}),lgr=Ya(k.jsx("path",{d:"M20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4C12.76,4 13.5,4.11 14.2, 4.31L15.77,2.74C14.61,2.26 13.34,2 12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0, 0 22,12M7.91,10.08L6.5,11.5L11,16L21,6L19.59,4.58L11,13.17L7.91,10.08Z"})),cgr=Ya(k.jsx("path",{d:"M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z"})),ugr=Ya(k.jsx("path",{d:"M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"})),dgr=Ya(k.jsx("path",{d:"M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20, 12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10, 10 0 0,0 12,2M11,17H13V11H11V17Z"})),eui=Ya(k.jsx("path",{d:"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"})),hgr=n=>{const{variant:e,color:t,severity:i,classes:r}=n,o={root:["root",`color${ii(t||i)}`,`${e}${ii(t||i)}`,`${e}`],icon:["icon"],message:["message"],action:["action"]};return Fo(o,Ypr,r)},fgr=tn(Jf,{name:"MuiAlert",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.variant],e[`${t.variant}${ii(t.color||t.severity)}`]]}})(Gs(({theme:n})=>{const e=n.palette.mode==="light"?n.darken:n.lighten,t=n.palette.mode==="light"?n.lighten:n.darken;return{...n.typography.body2,backgroundColor:"transparent",display:"flex",padding:"6px 16px",variants:[...Object.entries(n.palette).filter(Vh(["light"])).map(([i])=>({props:{colorSeverity:i,variant:"standard"},style:{color:n.vars?n.vars.palette.Alert[`${i}Color`]:e(n.palette[i].light,.6),backgroundColor:n.vars?n.vars.palette.Alert[`${i}StandardBg`]:t(n.palette[i].light,.9),[`& .${uPn.icon}`]:n.vars?{color:n.vars.palette.Alert[`${i}IconColor`]}:{color:n.palette[i].main}}})),...Object.entries(n.palette).filter(Vh(["light"])).map(([i])=>({props:{colorSeverity:i,variant:"outlined"},style:{color:n.vars?n.vars.palette.Alert[`${i}Color`]:e(n.palette[i].light,.6),border:`1px solid ${(n.vars||n).palette[i].light}`,[`& .${uPn.icon}`]:n.vars?{color:n.vars.palette.Alert[`${i}IconColor`]}:{color:n.palette[i].main}}})),...Object.entries(n.palette).filter(Vh(["dark"])).map(([i])=>({props:{colorSeverity:i,variant:"filled"},style:{fontWeight:n.typography.fontWeightMedium,...n.vars?{color:n.vars.palette.Alert[`${i}FilledColor`],backgroundColor:n.vars.palette.Alert[`${i}FilledBg`]}:{backgroundColor:n.palette.mode==="dark"?n.palette[i].dark:n.palette[i].main,color:n.palette.getContrastText(n.palette[i].main)}}}))]}})),pgr=tn("div",{name:"MuiAlert",slot:"Icon"})({marginRight:12,padding:"7px 0",display:"flex",fontSize:22,opacity:.9}),ggr=tn("div",{name:"MuiAlert",slot:"Message"})({padding:"8px 0",minWidth:0,overflow:"auto"}),mgr=tn("div",{name:"MuiAlert",slot:"Action"})({display:"flex",alignItems:"flex-start",padding:"4px 0 0 16px",marginLeft:"auto",marginRight:-8}),hPn={success:k.jsx(lgr,{fontSize:"inherit"}),warning:k.jsx(cgr,{fontSize:"inherit"}),error:k.jsx(ugr,{fontSize:"inherit"}),info:k.jsx(dgr,{fontSize:"inherit"})},b5e=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiAlert"}),{action:r,children:o,className:l,closeText:c="Close",color:d,components:h={},componentsProps:p={},icon:m,iconMapping:b=hPn,onClose:w,role:_="alert",severity:x="success",slotProps:T={},slots:I={},variant:L="standard",...A}=i,M={...i,color:d,severity:x,variant:L,colorSeverity:d||x},O=hgr(M),F={slots:{closeButton:h.CloseButton,closeIcon:h.CloseIcon,...I},slotProps:{...p,...T}},[j,W]=_o("root",{ref:t,shouldForwardComponentProp:!0,className:_i(O.root,l),elementType:fgr,externalForwardedProps:{...F,...A},ownerState:M,additionalProps:{role:_,elevation:0}}),[q,Z]=_o("icon",{className:O.icon,elementType:pgr,externalForwardedProps:F,ownerState:M}),[ee,G]=_o("message",{className:O.message,elementType:ggr,externalForwardedProps:F,ownerState:M}),[te,Q]=_o("action",{className:O.action,elementType:mgr,externalForwardedProps:F,ownerState:M}),[ie,se]=_o("closeButton",{elementType:da,externalForwardedProps:F,ownerState:M}),[de,ne]=_o("closeIcon",{elementType:eui,externalForwardedProps:F,ownerState:M});return k.jsxs(j,{...W,children:[m!==!1?k.jsx(q,{...Z,children:m||b[x]||hPn[x]}):null,k.jsx(ee,{...G,children:o}),r!=null?k.jsx(te,{...Q,children:r}):null,r==null&&w?k.jsx(te,{...Q,children:k.jsx(ie,{size:"small","aria-label":c,title:c,color:"inherit",onClick:w,...se,children:k.jsx(de,{fontSize:"small",...ne})})}):null]})});function bgr(n){return No("MuiTypography",n)}const fPn=Po("MuiTypography",["root","h1","h2","h3","h4","h5","h6","subtitle1","subtitle2","body1","body2","inherit","button","caption","overline","alignLeft","alignRight","alignCenter","alignJustify","noWrap","gutterBottom","paragraph"]),vgr={primary:!0,secondary:!0,error:!0,info:!0,success:!0,warning:!0,textPrimary:!0,textSecondary:!0,textDisabled:!0},wgr=Hhr(),ygr=n=>{const{align:e,gutterBottom:t,noWrap:i,paragraph:r,variant:o,classes:l}=n,c={root:["root",o,n.align!=="inherit"&&`align${ii(e)}`,t&&"gutterBottom",i&&"noWrap",r&&"paragraph"]};return Fo(c,bgr,l)},_gr=tn("span",{name:"MuiTypography",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.variant&&e[t.variant],t.align!=="inherit"&&e[`align${ii(t.align)}`],t.noWrap&&e.noWrap,t.gutterBottom&&e.gutterBottom,t.paragraph&&e.paragraph]}})(Gs(({theme:n})=>({margin:0,variants:[{props:{variant:"inherit"},style:{font:"inherit",lineHeight:"inherit",letterSpacing:"inherit"}},...Object.entries(n.typography).filter(([e,t])=>e!=="inherit"&&t&&typeof t=="object").map(([e,t])=>({props:{variant:e},style:t})),...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{color:(n.vars||n).palette[e].main}})),...Object.entries(n.palette?.text||{}).filter(([,e])=>typeof e=="string").map(([e])=>({props:{color:`text${ii(e)}`},style:{color:(n.vars||n).palette.text[e]}})),{props:({ownerState:e})=>e.align!=="inherit",style:{textAlign:"var(--Typography-textAlign)"}},{props:({ownerState:e})=>e.noWrap,style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}},{props:({ownerState:e})=>e.gutterBottom,style:{marginBottom:"0.35em"}},{props:({ownerState:e})=>e.paragraph,style:{marginBottom:16}}]}))),pPn={h1:"h1",h2:"h2",h3:"h3",h4:"h4",h5:"h5",h6:"h6",subtitle1:"h6",subtitle2:"h6",body1:"p",body2:"p",inherit:"p"},di=D.forwardRef(function(e,t){const{color:i,...r}=Wo({props:e,name:"MuiTypography"}),o=!vgr[i],l=wgr({...r,...o&&{color:i}}),{align:c="inherit",className:d,component:h,gutterBottom:p=!1,noWrap:m=!1,paragraph:b=!1,variant:w="body1",variantMapping:_=pPn,...x}=l,T={...l,align:c,color:i,className:d,component:h,gutterBottom:p,noWrap:m,paragraph:b,variant:w,variantMapping:_},I=h||(b?"p":_[w]||pPn[w])||"span",L=ygr(T);return k.jsx(_gr,{as:I,ref:t,className:_i(L.root,d),...x,ownerState:T,style:{...c!=="inherit"&&{"--Typography-textAlign":c},...x.style}})});function Cgr(n){return No("MuiAlertTitle",n)}Po("MuiAlertTitle",["root"]);const Sgr=n=>{const{classes:e}=n;return Fo({root:["root"]},Cgr,e)},xgr=tn(di,{name:"MuiAlertTitle",slot:"Root"})(Gs(({theme:n})=>({fontWeight:n.typography.fontWeightMedium,marginTop:-2}))),Egr=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiAlertTitle"}),{className:r,...o}=i,l=i,c=Sgr(l);return k.jsx(xgr,{gutterBottom:!0,component:"div",ownerState:l,ref:t,className:_i(c.root,r),...o})});function kgr(n){return No("MuiAppBar",n)}Po("MuiAppBar",["root","positionFixed","positionAbsolute","positionSticky","positionStatic","positionRelative","colorDefault","colorPrimary","colorSecondary","colorInherit","colorTransparent","colorError","colorInfo","colorSuccess","colorWarning"]);const Tgr=n=>{const{color:e,position:t,classes:i}=n,r={root:["root",`color${ii(e)}`,`position${ii(t)}`]};return Fo(r,kgr,i)},gPn=(n,e)=>n?`${n.replace(")","")}, ${e})`:e,Lgr=tn(Jf,{name:"MuiAppBar",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[`position${ii(t.position)}`],e[`color${ii(t.color)}`]]}})(Gs(({theme:n})=>({display:"flex",flexDirection:"column",width:"100%",boxSizing:"border-box",flexShrink:0,variants:[{props:{position:"fixed"},style:{position:"fixed",zIndex:(n.vars||n).zIndex.appBar,top:0,left:"auto",right:0,"@media print":{position:"absolute"}}},{props:{position:"absolute"},style:{position:"absolute",zIndex:(n.vars||n).zIndex.appBar,top:0,left:"auto",right:0}},{props:{position:"sticky"},style:{position:"sticky",zIndex:(n.vars||n).zIndex.appBar,top:0,left:"auto",right:0}},{props:{position:"static"},style:{position:"static"}},{props:{position:"relative"},style:{position:"relative"}},{props:{color:"inherit"},style:{"--AppBar-color":"inherit",color:"var(--AppBar-color)"}},{props:{color:"default"},style:{"--AppBar-background":n.vars?n.vars.palette.AppBar.defaultBg:n.palette.grey[100],"--AppBar-color":n.vars?n.vars.palette.text.primary:n.palette.getContrastText(n.palette.grey[100]),...n.applyStyles("dark",{"--AppBar-background":n.vars?n.vars.palette.AppBar.defaultBg:n.palette.grey[900],"--AppBar-color":n.vars?n.vars.palette.text.primary:n.palette.getContrastText(n.palette.grey[900])})}},...Object.entries(n.palette).filter(Vh(["contrastText"])).map(([e])=>({props:{color:e},style:{"--AppBar-background":(n.vars??n).palette[e].main,"--AppBar-color":(n.vars??n).palette[e].contrastText}})),{props:e=>e.enableColorOnDark===!0&&!["inherit","transparent"].includes(e.color),style:{backgroundColor:"var(--AppBar-background)",color:"var(--AppBar-color)"}},{props:e=>e.enableColorOnDark===!1&&!["inherit","transparent"].includes(e.color),style:{backgroundColor:"var(--AppBar-background)",color:"var(--AppBar-color)",...n.applyStyles("dark",{backgroundColor:n.vars?gPn(n.vars.palette.AppBar.darkBg,"var(--AppBar-background)"):null,color:n.vars?gPn(n.vars.palette.AppBar.darkColor,"var(--AppBar-color)"):null})}},{props:{color:"transparent"},style:{"--AppBar-background":"transparent","--AppBar-color":"inherit",backgroundColor:"var(--AppBar-background)",color:"var(--AppBar-color)",...n.applyStyles("dark",{backgroundImage:"none"})}}]}))),Dgr=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiAppBar"}),{className:r,color:o="primary",enableColorOnDark:l=!1,position:c="fixed",...d}=i,h={...i,color:o,position:c,enableColorOnDark:l},p=Tgr(h);return k.jsx(Lgr,{square:!0,component:"header",ownerState:h,elevation:4,className:_i(p.root,r,c==="fixed"&&"mui-fixed"),ref:t,...d})});function Jzt(n){const e=D.useRef({});return D.useEffect(()=>{e.current=n}),e.current}function mPn({array1:n,array2:e,parser:t=i=>i}){return n&&e&&n.length===e.length&&n.every((i,r)=>t(i)===t(e[r]))}function bPn(n){return n.normalize("NFD").replace(/[\u0300-\u036f]/g,"")}function eUt(n={}){const{ignoreAccents:e=!0,ignoreCase:t=!0,limit:i,matchFrom:r="any",stringify:o,trim:l=!1}=n;return(c,{inputValue:d,getOptionLabel:h})=>{let p=l?d.trim():d;t&&(p=p.toLowerCase()),e&&(p=bPn(p));const m=p?c.filter(b=>{let w=(o||h)(b);return t&&(w=w.toLowerCase()),e&&(w=bPn(w)),r==="start"?w.startsWith(p):w.includes(p)}):c;return typeof i=="number"?m.slice(0,i):m}}const Igr=eUt(),vPn=5,Agr=n=>n.current!==null&&n.current.parentElement?.contains(document.activeElement),Rgr=[];function wPn(n,e,t,i){if(e||n==null||i)return"";const r=t(n);return typeof r=="string"?r:""}function Mgr(n){const{unstable_isActiveElementInListbox:e=Agr,unstable_classNamePrefix:t="Mui",autoComplete:i=!1,autoHighlight:r=!1,autoSelect:o=!1,blurOnSelect:l=!1,clearOnBlur:c=!n.freeSolo,clearOnEscape:d=!1,componentName:h="useAutocomplete",defaultValue:p=n.multiple?Rgr:null,disableClearable:m=!1,disableCloseOnSelect:b=!1,disabled:w,disabledItemsFocusable:_=!1,disableListWrap:x=!1,filterOptions:T=Igr,filterSelectedOptions:I=!1,freeSolo:L=!1,getOptionDisabled:A,getOptionKey:M,getOptionLabel:O=cn=>cn.label??cn,groupBy:F,handleHomeEndKeys:j=!n.freeSolo,id:W,includeInputInList:q=!1,inputValue:Z,isOptionEqualToValue:ee=(cn,bt)=>cn===bt,multiple:G=!1,onChange:te,onClose:Q,onHighlightChange:ie,onInputChange:se,onOpen:de,open:ne,openOnFocus:we=!1,options:ue,readOnly:ce=!1,renderValue:ye,selectOnFocus:he=!n.freeSolo,value:pe}=n,me=YW(W);let be=O;be=cn=>{const bt=O(cn);return typeof bt!="string"?String(bt):bt};const xe=D.useRef(!1),Te=D.useRef(!0),Ge=D.useRef(null),tt=D.useRef(null),[Ue,Me]=D.useState(null),[He,at]=D.useState(-1),rt=r?0:-1,Be=D.useRef(rt),lt=D.useRef(wPn(p??pe,G,be)).current,[ct,ze]=S9({controlled:pe,default:p,name:h}),[Ke,$e]=S9({controlled:Z,default:lt,name:h,state:"inputValue"}),[nt,vt]=D.useState(!1),Pt=D.useCallback((cn,bt,_n)=>{if(!(G?ct.length!(I&&(G?ct:[ct]).some(bt=>bt!==null&&ee(cn,bt)))),{inputValue:mn&&wt?"":Ke,getOptionLabel:be}):[],Zi=Jzt({filteredOptions:hn,value:ct,inputValue:Ke});D.useEffect(()=>{const cn=ct!==Zi.value;nt&&!cn||L&&!cn||Pt(null,ct,"reset")},[ct,Pt,nt,Zi.value,L]);const $i=Ct&&hn.length>0&&!ce,Dr=ub(cn=>{if(cn===-1)Ge.current.focus();else{const bt=ye?"data-item-index":"data-tag-index";Ue.querySelector(`[${bt}="${cn}"]`).focus()}});D.useEffect(()=>{G&&He>ct.length-1&&(at(-1),Dr(-1))},[ct,G,He,Dr]);function ps(cn,bt){if(!tt.current||cn<0||cn>=hn.length)return-1;let _n=cn;for(;;){const Di=tt.current.querySelector(`[data-option-index="${_n}"]`),Ni=_?!1:!Di||Di.disabled||Di.getAttribute("aria-disabled")==="true";if(Di&&Di.hasAttribute("tabindex")&&!Ni)return _n;if(bt==="next"?_n=(_n+1)%hn.length:_n=(_n-1+hn.length)%hn.length,_n===cn)return-1}}const nn=ub(({event:cn,index:bt,reason:_n})=>{if(Be.current=bt,bt===-1?Ge.current.removeAttribute("aria-activedescendant"):Ge.current.setAttribute("aria-activedescendant",`${me}-option-${bt}`),ie&&["mouse","keyboard","touch"].includes(_n)&&ie(cn,bt===-1?null:hn[bt],_n),!tt.current)return;const Di=tt.current.querySelector(`[role="option"].${t}-focused`);Di&&(Di.classList.remove(`${t}-focused`),Di.classList.remove(`${t}-focusVisible`));let Ni=tt.current;if(tt.current.getAttribute("role")!=="listbox"&&(Ni=tt.current.parentElement.querySelector('[role="listbox"]')),!Ni)return;if(bt===-1){Ni.scrollTop=0;return}const Ds=tt.current.querySelector(`[data-option-index="${bt}"]`);if(Ds&&(Ds.classList.add(`${t}-focused`),_n==="keyboard"&&Ds.classList.add(`${t}-focusVisible`),Ni.scrollHeight>Ni.clientHeight&&_n!=="mouse"&&_n!=="touch")){const Es=Ds,$a=Ni.clientHeight+Ni.scrollTop,Lu=Es.offsetTop+Es.offsetHeight;Lu>$a?Ni.scrollTop=Lu-Ni.clientHeight:Es.offsetTop-Es.offsetHeight*(F?1.3:0){if(!xn)return;const Ds=ps((()=>{const Es=hn.length-1;if(bt==="reset")return rt;if(bt==="start")return 0;if(bt==="end")return Es;const $a=Be.current+bt;return $a<0?$a===-1&&q?-1:x&&Be.current!==-1||Math.abs(bt)>1?0:Es:$a>Es?$a===Es+1&&q?-1:x||Math.abs(bt)>1?Es:0:$a})(),_n);if(nn({index:Ds,reason:Di,event:cn}),i&&bt!=="reset")if(Ds===-1)Ge.current.value=Ke;else{const Es=be(hn[Ds]);Ge.current.value=Es,Es.toLowerCase().indexOf(Ke.toLowerCase())===0&&Ke.length>0&&Ge.current.setSelectionRange(Ke.length,Es.length)}}),Ei=!mPn({array1:Zi.filteredOptions,array2:hn,parser:be}),gr=()=>{const cn=(bt,_n)=>{const Di=bt?be(bt):"",Ni=_n?be(_n):"";return Di===Ni};if(Be.current!==-1&&!mPn({array1:Zi.filteredOptions,array2:hn,parser:be})&&Zi.inputValue===Ke&&(G?ct.length===Zi.value.length&&Zi.value.every((bt,_n)=>be(ct[_n])===be(bt)):cn(Zi.value,ct))){const bt=Zi.filteredOptions[Be.current];if(bt)return hn.findIndex(_n=>be(_n)===be(bt))}return-1},ss=D.useCallback(()=>{if(!xn)return;const cn=gr();if(cn!==-1){Be.current=cn;return}const bt=G?ct[0]:ct;if(hn.length===0||bt==null){xt({diff:"reset"});return}if(tt.current){if(bt!=null){const _n=hn[Be.current];if(G&&_n&&ct.findIndex(Ni=>ee(_n,Ni))!==-1)return;const Di=hn.findIndex(Ni=>ee(Ni,bt));Di===-1?xt({diff:"reset"}):nn({index:Di});return}if(Be.current>=hn.length-1){nn({index:hn.length-1});return}nn({index:Be.current})}},[hn.length,G?!1:ct,xt,nn,xn,Ke,G]),us=ub(cn=>{UOt(tt,cn),cn&&ss()});D.useEffect(()=>{(Ei||xn&&!b)&&ss()},[ss,Ei,xn,b]);const _r=cn=>{Ct||(Ye(!0),zt(!0),de&&de(cn))},uo=(cn,bt)=>{Ct&&(Ye(!1),Q&&Q(cn,bt))},xs=(cn,bt,_n,Di)=>{if(G){if(ct.length===bt.length&&ct.every((Ni,Ds)=>Ni===bt[Ds]))return}else if(ct===bt)return;te&&te(cn,bt,_n,Di),ze(bt)},Fs=D.useRef(!1),eo=(cn,bt,_n="selectOption",Di="options")=>{let Ni=_n,Ds=bt;if(G){Ds=Array.isArray(ct)?ct.slice():[];const Es=Ds.findIndex($a=>ee(bt,$a));Es===-1?Ds.push(bt):Di!=="freeSolo"&&(Ds.splice(Es,1),Ni="removeOption")}Pt(cn,Ds,Ni),xs(cn,Ds,Ni,{option:bt}),!b&&(!cn||!cn.ctrlKey&&!cn.metaKey)&&uo(cn,Ni),(l===!0||l==="touch"&&Fs.current||l==="mouse"&&!Fs.current)&&Ge.current.blur()};function Ri(cn,bt){if(cn===-1)return-1;let _n=cn;for(;;){if(bt==="next"&&_n===ct.length||bt==="previous"&&_n===-1)return-1;const Di=ye?"data-item-index":"data-tag-index",Ni=Ue.querySelector(`[${Di}="${_n}"]`);if(!Ni||!Ni.hasAttribute("tabindex")||Ni.disabled||Ni.getAttribute("aria-disabled")==="true")_n+=bt==="next"?1:-1;else return _n}}const Ls=(cn,bt)=>{if(!G)return;Ke===""&&uo(cn,"toggleInput");let _n=He;He===-1&&bt==="previous"?(_n=ct.length-1,L&&Ke!==""&&($e(""),se&&se(cn,"","reset"))):(_n+=bt==="next"?1:-1,_n<0&&(_n=0),_n===ct.length&&(_n=-1)),_n=Ri(_n,bt),at(_n),Dr(_n)},Cr=cn=>{xe.current=!0,$e(""),se&&se(cn,"","clear"),xs(cn,G?[]:null,"clear")},Sr=cn=>bt=>{if(cn.onKeyDown&&cn.onKeyDown(bt),!bt.defaultMuiPrevented&&(He!==-1&&!["ArrowLeft","ArrowRight"].includes(bt.key)&&(at(-1),Dr(-1)),bt.which!==229))switch(bt.key){case"Home":xn&&j&&(bt.preventDefault(),xt({diff:"start",direction:"next",reason:"keyboard",event:bt}));break;case"End":xn&&j&&(bt.preventDefault(),xt({diff:"end",direction:"previous",reason:"keyboard",event:bt}));break;case"PageUp":bt.preventDefault(),xt({diff:-vPn,direction:"previous",reason:"keyboard",event:bt}),_r(bt);break;case"PageDown":bt.preventDefault(),xt({diff:vPn,direction:"next",reason:"keyboard",event:bt}),_r(bt);break;case"ArrowDown":bt.preventDefault(),xt({diff:1,direction:"next",reason:"keyboard",event:bt}),_r(bt);break;case"ArrowUp":bt.preventDefault(),xt({diff:-1,direction:"previous",reason:"keyboard",event:bt}),_r(bt);break;case"ArrowLeft":{const _n=Ge.current;if(!(_n&&_n.selectionStart===0&&_n.selectionEnd===0))return;!G&&ye&&ct!=null?(L&&Ke!==""&&($e(""),se&&se(bt,"","reset")),at(0),Dr(0)):Ls(bt,"previous");break}case"ArrowRight":!G&&ye?(at(-1),Dr(-1)):Ls(bt,"next");break;case"Enter":if(Be.current!==-1&&xn){const _n=hn[Be.current],Di=A?A(_n):!1;if(bt.preventDefault(),Di)return;eo(bt,_n,"selectOption"),i&&Ge.current.setSelectionRange(Ge.current.value.length,Ge.current.value.length)}else L&&Ke!==""&&mn===!1&&(G&&bt.preventDefault(),eo(bt,Ke,"createOption","freeSolo"));break;case"Escape":xn?(bt.preventDefault(),bt.stopPropagation(),uo(bt,"escape")):d&&(Ke!==""||G&&ct.length>0||ye)&&(bt.preventDefault(),bt.stopPropagation(),Cr(bt));break;case"Backspace":if(G&&!ce&&Ke===""&&ct.length>0){const _n=He===-1?ct.length-1:He,Di=ct.slice();Di.splice(_n,1),xs(bt,Di,"removeOption",{option:ct[_n]})}!G&&ye&&!ce&&Ke===""&&xs(bt,null,"removeOption",{option:ct});break;case"Delete":if(G&&!ce&&Ke===""&&ct.length>0&&He!==-1){const _n=He,Di=ct.slice();Di.splice(_n,1),xs(bt,Di,"removeOption",{option:ct[_n]})}!G&&ye&&!ce&&Ke===""&&xs(bt,null,"removeOption",{option:ct});break}},os=cn=>{vt(!0),He!==-1&&(at(-1),Dr(-1)),we&&!xe.current&&_r(cn)},Ks=cn=>{if(e(tt)){Ge.current.focus();return}vt(!1),Te.current=!0,xe.current=!1,o&&Be.current!==-1&&xn?eo(cn,hn[Be.current],"blur"):o&&L&&Ke!==""?eo(cn,Ke,"blur","freeSolo"):c&&Pt(cn,ct,"blur"),uo(cn,"blur")},Ft=cn=>{const bt=cn.target.value;Ke!==bt&&($e(bt),zt(!1),se&&se(cn,bt,"input")),bt===""?!m&&!G&&!ye&&xs(cn,null,"clear"):_r(cn)},rn=cn=>{const bt=Number(cn.currentTarget.getAttribute("data-option-index"));Be.current!==bt&&nn({event:cn,index:bt,reason:"mouse"})},zn=cn=>{nn({event:cn,index:Number(cn.currentTarget.getAttribute("data-option-index")),reason:"touch"}),Fs.current=!0},Oi=cn=>{const bt=Number(cn.currentTarget.getAttribute("data-option-index"));eo(cn,hn[bt],"selectOption"),Fs.current=!1},Ki=cn=>bt=>{const _n=ct.slice();_n.splice(cn,1),xs(bt,_n,"removeOption",{option:ct[cn]})},gs=cn=>{xs(cn,null,"removeOption",{option:ct})},ur=cn=>{Ct?uo(cn,"toggleInput"):_r(cn)},vs=cn=>{cn.currentTarget.contains(cn.target)&&cn.target.getAttribute("id")!==me&&cn.preventDefault()},Ir=cn=>{cn.currentTarget.contains(cn.target)&&(Ge.current.focus(),he&&Te.current&&Ge.current.selectionEnd-Ge.current.selectionStart===0&&Ge.current.select(),Te.current=!1)},oo=cn=>{!w&&(Ke===""||!Ct)&&ur(cn)};let Do=L&&Ke.length>0;Do=Do||(G?ct.length>0:ct!==null);let zs=hn;return F&&(zs=hn.reduce((cn,bt,_n)=>{const Di=F(bt);return cn.length>0&&cn[cn.length-1].group===Di?cn[cn.length-1].options.push(bt):cn.push({key:_n,index:_n,group:Di,options:[bt]}),cn},[])),w&&nt&&Ks(),{getRootProps:(cn={})=>({...cn,onKeyDown:Sr(cn),onMouseDown:vs,onClick:Ir}),getInputLabelProps:()=>({id:`${me}-label`,htmlFor:me}),getInputProps:()=>({id:me,value:Ke,onBlur:Ks,onFocus:os,onChange:Ft,onMouseDown:oo,"aria-activedescendant":xn?"":null,"aria-autocomplete":i?"both":"list","aria-controls":$i?`${me}-listbox`:void 0,"aria-expanded":$i,autoComplete:"off",ref:Ge,autoCapitalize:"none",spellCheck:"false",role:"combobox",disabled:w}),getClearProps:()=>({tabIndex:-1,type:"button",onClick:Cr}),getItemProps:({index:cn=0}={})=>({...G&&{key:cn},...ye?{"data-item-index":cn}:{"data-tag-index":cn},tabIndex:-1,...!ce&&{onDelete:G?Ki(cn):gs}}),getPopupIndicatorProps:()=>({tabIndex:-1,type:"button",onClick:ur}),getTagProps:({index:cn})=>({key:cn,"data-tag-index":cn,tabIndex:-1,...!ce&&{onDelete:Ki(cn)}}),getListboxProps:()=>({role:"listbox",id:`${me}-listbox`,"aria-labelledby":`${me}-label`,"aria-multiselectable":G||void 0,ref:us,onMouseDown:cn=>{cn.preventDefault()}}),getOptionProps:({index:cn,option:bt})=>{const _n=(G?ct:[ct]).some(Ni=>Ni!=null&&ee(bt,Ni)),Di=A?A(bt):!1;return{key:M?.(bt)??be(bt),tabIndex:-1,role:"option",id:`${me}-option-${cn}`,onMouseMove:rn,onClick:Oi,onTouchStart:zn,"data-option-index":cn,"aria-disabled":Di,"aria-selected":_n}},id:me,inputValue:Ke,value:ct,dirty:Do,expanded:xn&&Ue,popupOpen:xn,focused:nt||He!==-1,anchorEl:Ue,setAnchorEl:Me,focusedItem:He,focusedTag:He,groupedOptions:zs}}var X4="top",I3="bottom",A3="right",Q4="left",tUt="auto",v5e=[X4,I3,A3,Q4],pme="start",pLe="end",Ogr="clippingParents",tui="viewport",Fxe="popper",Ngr="reference",yPn=v5e.reduce(function(n,e){return n.concat([e+"-"+pme,e+"-"+pLe])},[]),nui=[].concat(v5e,[tUt]).reduce(function(n,e){return n.concat([e,e+"-"+pme,e+"-"+pLe])},[]),Pgr="beforeRead",Fgr="read",jgr="afterRead",Hgr="beforeMain",Bgr="main",Wgr="afterMain",Vgr="beforeWrite",$gr="write",zgr="afterWrite",Ugr=[Pgr,Fgr,jgr,Hgr,Bgr,Wgr,Vgr,$gr,zgr];function $9(n){return n?(n.nodeName||"").toLowerCase():null}function Z6(n){if(n==null)return window;if(n.toString()!=="[object Window]"){var e=n.ownerDocument;return e&&e.defaultView||window}return n}function ese(n){var e=Z6(n).Element;return n instanceof e||n instanceof Element}function y3(n){var e=Z6(n).HTMLElement;return n instanceof e||n instanceof HTMLElement}function nUt(n){if(typeof ShadowRoot>"u")return!1;var e=Z6(n).ShadowRoot;return n instanceof e||n instanceof ShadowRoot}function qgr(n){var e=n.state;Object.keys(e.elements).forEach(function(t){var i=e.styles[t]||{},r=e.attributes[t]||{},o=e.elements[t];!y3(o)||!$9(o)||(Object.assign(o.style,i),Object.keys(r).forEach(function(l){var c=r[l];c===!1?o.removeAttribute(l):o.setAttribute(l,c===!0?"":c)}))})}function Ggr(n){var e=n.state,t={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,t.popper),e.styles=t,e.elements.arrow&&Object.assign(e.elements.arrow.style,t.arrow),function(){Object.keys(e.elements).forEach(function(i){var r=e.elements[i],o=e.attributes[i]||{},l=Object.keys(e.styles.hasOwnProperty(i)?e.styles[i]:t[i]),c=l.reduce(function(d,h){return d[h]="",d},{});!y3(r)||!$9(r)||(Object.assign(r.style,c),Object.keys(o).forEach(function(d){r.removeAttribute(d)}))})}}const Kgr={name:"applyStyles",enabled:!0,phase:"write",fn:qgr,effect:Ggr,requires:["computeStyles"]};function E9(n){return n.split("-")[0]}var wie=Math.max,sGe=Math.min,gme=Math.round;function ZOt(){var n=navigator.userAgentData;return n!=null&&n.brands&&Array.isArray(n.brands)?n.brands.map(function(e){return e.brand+"/"+e.version}).join(" "):navigator.userAgent}function iui(){return!/^((?!chrome|android).)*safari/i.test(ZOt())}function mme(n,e,t){e===void 0&&(e=!1),t===void 0&&(t=!1);var i=n.getBoundingClientRect(),r=1,o=1;e&&y3(n)&&(r=n.offsetWidth>0&&gme(i.width)/n.offsetWidth||1,o=n.offsetHeight>0&&gme(i.height)/n.offsetHeight||1);var l=ese(n)?Z6(n):window,c=l.visualViewport,d=!iui()&&t,h=(i.left+(d&&c?c.offsetLeft:0))/r,p=(i.top+(d&&c?c.offsetTop:0))/o,m=i.width/r,b=i.height/o;return{width:m,height:b,top:p,right:h+m,bottom:p+b,left:h,x:h,y:p}}function iUt(n){var e=mme(n),t=n.offsetWidth,i=n.offsetHeight;return Math.abs(e.width-t)<=1&&(t=e.width),Math.abs(e.height-i)<=1&&(i=e.height),{x:n.offsetLeft,y:n.offsetTop,width:t,height:i}}function rui(n,e){var t=e.getRootNode&&e.getRootNode();if(n.contains(e))return!0;if(t&&nUt(t)){var i=e;do{if(i&&n.isSameNode(i))return!0;i=i.parentNode||i.host}while(i)}return!1}function vW(n){return Z6(n).getComputedStyle(n)}function Ygr(n){return["table","td","th"].indexOf($9(n))>=0}function PY(n){return((ese(n)?n.ownerDocument:n.document)||window.document).documentElement}function Vet(n){return $9(n)==="html"?n:n.assignedSlot||n.parentNode||(nUt(n)?n.host:null)||PY(n)}function _Pn(n){return!y3(n)||vW(n).position==="fixed"?null:n.offsetParent}function Zgr(n){var e=/firefox/i.test(ZOt()),t=/Trident/i.test(ZOt());if(t&&y3(n)){var i=vW(n);if(i.position==="fixed")return null}var r=Vet(n);for(nUt(r)&&(r=r.host);y3(r)&&["html","body"].indexOf($9(r))<0;){var o=vW(r);if(o.transform!=="none"||o.perspective!=="none"||o.contain==="paint"||["transform","perspective"].indexOf(o.willChange)!==-1||e&&o.willChange==="filter"||e&&o.filter&&o.filter!=="none")return r;r=r.parentNode}return null}function w5e(n){for(var e=Z6(n),t=_Pn(n);t&&Ygr(t)&&vW(t).position==="static";)t=_Pn(t);return t&&($9(t)==="html"||$9(t)==="body"&&vW(t).position==="static")?e:t||Zgr(n)||e}function rUt(n){return["top","bottom"].indexOf(n)>=0?"x":"y"}function s4e(n,e,t){return wie(n,sGe(e,t))}function Xgr(n,e,t){var i=s4e(n,e,t);return i>t?t:i}function sui(){return{top:0,right:0,bottom:0,left:0}}function oui(n){return Object.assign({},sui(),n)}function aui(n,e){return e.reduce(function(t,i){return t[i]=n,t},{})}var Qgr=function(e,t){return e=typeof e=="function"?e(Object.assign({},t.rects,{placement:t.placement})):e,oui(typeof e!="number"?e:aui(e,v5e))};function Jgr(n){var e,t=n.state,i=n.name,r=n.options,o=t.elements.arrow,l=t.modifiersData.popperOffsets,c=E9(t.placement),d=rUt(c),h=[Q4,A3].indexOf(c)>=0,p=h?"height":"width";if(!(!o||!l)){var m=Qgr(r.padding,t),b=iUt(o),w=d==="y"?X4:Q4,_=d==="y"?I3:A3,x=t.rects.reference[p]+t.rects.reference[d]-l[d]-t.rects.popper[p],T=l[d]-t.rects.reference[d],I=w5e(o),L=I?d==="y"?I.clientHeight||0:I.clientWidth||0:0,A=x/2-T/2,M=m[w],O=L-b[p]-m[_],F=L/2-b[p]/2+A,j=s4e(M,F,O),W=d;t.modifiersData[i]=(e={},e[W]=j,e.centerOffset=j-F,e)}}function emr(n){var e=n.state,t=n.options,i=t.element,r=i===void 0?"[data-popper-arrow]":i;r!=null&&(typeof r=="string"&&(r=e.elements.popper.querySelector(r),!r)||rui(e.elements.popper,r)&&(e.elements.arrow=r))}const tmr={name:"arrow",enabled:!0,phase:"main",fn:Jgr,effect:emr,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function bme(n){return n.split("-")[1]}var nmr={top:"auto",right:"auto",bottom:"auto",left:"auto"};function imr(n,e){var t=n.x,i=n.y,r=e.devicePixelRatio||1;return{x:gme(t*r)/r||0,y:gme(i*r)/r||0}}function CPn(n){var e,t=n.popper,i=n.popperRect,r=n.placement,o=n.variation,l=n.offsets,c=n.position,d=n.gpuAcceleration,h=n.adaptive,p=n.roundOffsets,m=n.isFixed,b=l.x,w=b===void 0?0:b,_=l.y,x=_===void 0?0:_,T=typeof p=="function"?p({x:w,y:x}):{x:w,y:x};w=T.x,x=T.y;var I=l.hasOwnProperty("x"),L=l.hasOwnProperty("y"),A=Q4,M=X4,O=window;if(h){var F=w5e(t),j="clientHeight",W="clientWidth";if(F===Z6(t)&&(F=PY(t),vW(F).position!=="static"&&c==="absolute"&&(j="scrollHeight",W="scrollWidth")),F=F,r===X4||(r===Q4||r===A3)&&o===pLe){M=I3;var q=m&&F===O&&O.visualViewport?O.visualViewport.height:F[j];x-=q-i.height,x*=d?1:-1}if(r===Q4||(r===X4||r===I3)&&o===pLe){A=A3;var Z=m&&F===O&&O.visualViewport?O.visualViewport.width:F[W];w-=Z-i.width,w*=d?1:-1}}var ee=Object.assign({position:c},h&&nmr),G=p===!0?imr({x:w,y:x},Z6(t)):{x:w,y:x};if(w=G.x,x=G.y,d){var te;return Object.assign({},ee,(te={},te[M]=L?"0":"",te[A]=I?"0":"",te.transform=(O.devicePixelRatio||1)<=1?"translate("+w+"px, "+x+"px)":"translate3d("+w+"px, "+x+"px, 0)",te))}return Object.assign({},ee,(e={},e[M]=L?x+"px":"",e[A]=I?w+"px":"",e.transform="",e))}function rmr(n){var e=n.state,t=n.options,i=t.gpuAcceleration,r=i===void 0?!0:i,o=t.adaptive,l=o===void 0?!0:o,c=t.roundOffsets,d=c===void 0?!0:c,h={placement:E9(e.placement),variation:bme(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:r,isFixed:e.options.strategy==="fixed"};e.modifiersData.popperOffsets!=null&&(e.styles.popper=Object.assign({},e.styles.popper,CPn(Object.assign({},h,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:l,roundOffsets:d})))),e.modifiersData.arrow!=null&&(e.styles.arrow=Object.assign({},e.styles.arrow,CPn(Object.assign({},h,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:d})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})}const smr={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:rmr,data:{}};var MBe={passive:!0};function omr(n){var e=n.state,t=n.instance,i=n.options,r=i.scroll,o=r===void 0?!0:r,l=i.resize,c=l===void 0?!0:l,d=Z6(e.elements.popper),h=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&h.forEach(function(p){p.addEventListener("scroll",t.update,MBe)}),c&&d.addEventListener("resize",t.update,MBe),function(){o&&h.forEach(function(p){p.removeEventListener("scroll",t.update,MBe)}),c&&d.removeEventListener("resize",t.update,MBe)}}const amr={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:omr,data:{}};var lmr={left:"right",right:"left",bottom:"top",top:"bottom"};function wUe(n){return n.replace(/left|right|bottom|top/g,function(e){return lmr[e]})}var cmr={start:"end",end:"start"};function SPn(n){return n.replace(/start|end/g,function(e){return cmr[e]})}function sUt(n){var e=Z6(n),t=e.pageXOffset,i=e.pageYOffset;return{scrollLeft:t,scrollTop:i}}function oUt(n){return mme(PY(n)).left+sUt(n).scrollLeft}function umr(n,e){var t=Z6(n),i=PY(n),r=t.visualViewport,o=i.clientWidth,l=i.clientHeight,c=0,d=0;if(r){o=r.width,l=r.height;var h=iui();(h||!h&&e==="fixed")&&(c=r.offsetLeft,d=r.offsetTop)}return{width:o,height:l,x:c+oUt(n),y:d}}function dmr(n){var e,t=PY(n),i=sUt(n),r=(e=n.ownerDocument)==null?void 0:e.body,o=wie(t.scrollWidth,t.clientWidth,r?r.scrollWidth:0,r?r.clientWidth:0),l=wie(t.scrollHeight,t.clientHeight,r?r.scrollHeight:0,r?r.clientHeight:0),c=-i.scrollLeft+oUt(n),d=-i.scrollTop;return vW(r||t).direction==="rtl"&&(c+=wie(t.clientWidth,r?r.clientWidth:0)-o),{width:o,height:l,x:c,y:d}}function aUt(n){var e=vW(n),t=e.overflow,i=e.overflowX,r=e.overflowY;return/auto|scroll|overlay|hidden/.test(t+r+i)}function lui(n){return["html","body","#document"].indexOf($9(n))>=0?n.ownerDocument.body:y3(n)&&aUt(n)?n:lui(Vet(n))}function o4e(n,e){var t;e===void 0&&(e=[]);var i=lui(n),r=i===((t=n.ownerDocument)==null?void 0:t.body),o=Z6(i),l=r?[o].concat(o.visualViewport||[],aUt(i)?i:[]):i,c=e.concat(l);return r?c:c.concat(o4e(Vet(l)))}function XOt(n){return Object.assign({},n,{left:n.x,top:n.y,right:n.x+n.width,bottom:n.y+n.height})}function hmr(n,e){var t=mme(n,!1,e==="fixed");return t.top=t.top+n.clientTop,t.left=t.left+n.clientLeft,t.bottom=t.top+n.clientHeight,t.right=t.left+n.clientWidth,t.width=n.clientWidth,t.height=n.clientHeight,t.x=t.left,t.y=t.top,t}function xPn(n,e,t){return e===tui?XOt(umr(n,t)):ese(e)?hmr(e,t):XOt(dmr(PY(n)))}function fmr(n){var e=o4e(Vet(n)),t=["absolute","fixed"].indexOf(vW(n).position)>=0,i=t&&y3(n)?w5e(n):n;return ese(i)?e.filter(function(r){return ese(r)&&rui(r,i)&&$9(r)!=="body"}):[]}function pmr(n,e,t,i){var r=e==="clippingParents"?fmr(n):[].concat(e),o=[].concat(r,[t]),l=o[0],c=o.reduce(function(d,h){var p=xPn(n,h,i);return d.top=wie(p.top,d.top),d.right=sGe(p.right,d.right),d.bottom=sGe(p.bottom,d.bottom),d.left=wie(p.left,d.left),d},xPn(n,l,i));return c.width=c.right-c.left,c.height=c.bottom-c.top,c.x=c.left,c.y=c.top,c}function cui(n){var e=n.reference,t=n.element,i=n.placement,r=i?E9(i):null,o=i?bme(i):null,l=e.x+e.width/2-t.width/2,c=e.y+e.height/2-t.height/2,d;switch(r){case X4:d={x:l,y:e.y-t.height};break;case I3:d={x:l,y:e.y+e.height};break;case A3:d={x:e.x+e.width,y:c};break;case Q4:d={x:e.x-t.width,y:c};break;default:d={x:e.x,y:e.y}}var h=r?rUt(r):null;if(h!=null){var p=h==="y"?"height":"width";switch(o){case pme:d[h]=d[h]-(e[p]/2-t[p]/2);break;case pLe:d[h]=d[h]+(e[p]/2-t[p]/2);break}}return d}function gLe(n,e){e===void 0&&(e={});var t=e,i=t.placement,r=i===void 0?n.placement:i,o=t.strategy,l=o===void 0?n.strategy:o,c=t.boundary,d=c===void 0?Ogr:c,h=t.rootBoundary,p=h===void 0?tui:h,m=t.elementContext,b=m===void 0?Fxe:m,w=t.altBoundary,_=w===void 0?!1:w,x=t.padding,T=x===void 0?0:x,I=oui(typeof T!="number"?T:aui(T,v5e)),L=b===Fxe?Ngr:Fxe,A=n.rects.popper,M=n.elements[_?L:b],O=pmr(ese(M)?M:M.contextElement||PY(n.elements.popper),d,p,l),F=mme(n.elements.reference),j=cui({reference:F,element:A,placement:r}),W=XOt(Object.assign({},A,j)),q=b===Fxe?W:F,Z={top:O.top-q.top+I.top,bottom:q.bottom-O.bottom+I.bottom,left:O.left-q.left+I.left,right:q.right-O.right+I.right},ee=n.modifiersData.offset;if(b===Fxe&&ee){var G=ee[r];Object.keys(Z).forEach(function(te){var Q=[A3,I3].indexOf(te)>=0?1:-1,ie=[X4,I3].indexOf(te)>=0?"y":"x";Z[te]+=G[ie]*Q})}return Z}function gmr(n,e){e===void 0&&(e={});var t=e,i=t.placement,r=t.boundary,o=t.rootBoundary,l=t.padding,c=t.flipVariations,d=t.allowedAutoPlacements,h=d===void 0?nui:d,p=bme(i),m=p?c?yPn:yPn.filter(function(_){return bme(_)===p}):v5e,b=m.filter(function(_){return h.indexOf(_)>=0});b.length===0&&(b=m);var w=b.reduce(function(_,x){return _[x]=gLe(n,{placement:x,boundary:r,rootBoundary:o,padding:l})[E9(x)],_},{});return Object.keys(w).sort(function(_,x){return w[_]-w[x]})}function mmr(n){if(E9(n)===tUt)return[];var e=wUe(n);return[SPn(n),e,SPn(e)]}function bmr(n){var e=n.state,t=n.options,i=n.name;if(!e.modifiersData[i]._skip){for(var r=t.mainAxis,o=r===void 0?!0:r,l=t.altAxis,c=l===void 0?!0:l,d=t.fallbackPlacements,h=t.padding,p=t.boundary,m=t.rootBoundary,b=t.altBoundary,w=t.flipVariations,_=w===void 0?!0:w,x=t.allowedAutoPlacements,T=e.options.placement,I=E9(T),L=I===T,A=d||(L||!_?[wUe(T)]:mmr(T)),M=[T].concat(A).reduce(function(pe,me){return pe.concat(E9(me)===tUt?gmr(e,{placement:me,boundary:p,rootBoundary:m,padding:h,flipVariations:_,allowedAutoPlacements:x}):me)},[]),O=e.rects.reference,F=e.rects.popper,j=new Map,W=!0,q=M[0],Z=0;Z=0,ie=Q?"width":"height",se=gLe(e,{placement:ee,boundary:p,rootBoundary:m,altBoundary:b,padding:h}),de=Q?te?A3:Q4:te?I3:X4;O[ie]>F[ie]&&(de=wUe(de));var ne=wUe(de),we=[];if(o&&we.push(se[G]<=0),c&&we.push(se[de]<=0,se[ne]<=0),we.every(function(pe){return pe})){q=ee,W=!1;break}j.set(ee,we)}if(W)for(var ue=_?3:1,ce=function(me){var be=M.find(function(xe){var Te=j.get(xe);if(Te)return Te.slice(0,me).every(function(Ge){return Ge})});if(be)return q=be,"break"},ye=ue;ye>0;ye--){var he=ce(ye);if(he==="break")break}e.placement!==q&&(e.modifiersData[i]._skip=!0,e.placement=q,e.reset=!0)}}const vmr={name:"flip",enabled:!0,phase:"main",fn:bmr,requiresIfExists:["offset"],data:{_skip:!1}};function EPn(n,e,t){return t===void 0&&(t={x:0,y:0}),{top:n.top-e.height-t.y,right:n.right-e.width+t.x,bottom:n.bottom-e.height+t.y,left:n.left-e.width-t.x}}function kPn(n){return[X4,A3,I3,Q4].some(function(e){return n[e]>=0})}function wmr(n){var e=n.state,t=n.name,i=e.rects.reference,r=e.rects.popper,o=e.modifiersData.preventOverflow,l=gLe(e,{elementContext:"reference"}),c=gLe(e,{altBoundary:!0}),d=EPn(l,i),h=EPn(c,r,o),p=kPn(d),m=kPn(h);e.modifiersData[t]={referenceClippingOffsets:d,popperEscapeOffsets:h,isReferenceHidden:p,hasPopperEscaped:m},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":p,"data-popper-escaped":m})}const ymr={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:wmr};function _mr(n,e,t){var i=E9(n),r=[Q4,X4].indexOf(i)>=0?-1:1,o=typeof t=="function"?t(Object.assign({},e,{placement:n})):t,l=o[0],c=o[1];return l=l||0,c=(c||0)*r,[Q4,A3].indexOf(i)>=0?{x:c,y:l}:{x:l,y:c}}function Cmr(n){var e=n.state,t=n.options,i=n.name,r=t.offset,o=r===void 0?[0,0]:r,l=nui.reduce(function(p,m){return p[m]=_mr(m,e.rects,o),p},{}),c=l[e.placement],d=c.x,h=c.y;e.modifiersData.popperOffsets!=null&&(e.modifiersData.popperOffsets.x+=d,e.modifiersData.popperOffsets.y+=h),e.modifiersData[i]=l}const Smr={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:Cmr};function xmr(n){var e=n.state,t=n.name;e.modifiersData[t]=cui({reference:e.rects.reference,element:e.rects.popper,placement:e.placement})}const Emr={name:"popperOffsets",enabled:!0,phase:"read",fn:xmr,data:{}};function kmr(n){return n==="x"?"y":"x"}function Tmr(n){var e=n.state,t=n.options,i=n.name,r=t.mainAxis,o=r===void 0?!0:r,l=t.altAxis,c=l===void 0?!1:l,d=t.boundary,h=t.rootBoundary,p=t.altBoundary,m=t.padding,b=t.tether,w=b===void 0?!0:b,_=t.tetherOffset,x=_===void 0?0:_,T=gLe(e,{boundary:d,rootBoundary:h,padding:m,altBoundary:p}),I=E9(e.placement),L=bme(e.placement),A=!L,M=rUt(I),O=kmr(M),F=e.modifiersData.popperOffsets,j=e.rects.reference,W=e.rects.popper,q=typeof x=="function"?x(Object.assign({},e.rects,{placement:e.placement})):x,Z=typeof q=="number"?{mainAxis:q,altAxis:q}:Object.assign({mainAxis:0,altAxis:0},q),ee=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,G={x:0,y:0};if(F){if(o){var te,Q=M==="y"?X4:Q4,ie=M==="y"?I3:A3,se=M==="y"?"height":"width",de=F[M],ne=de+T[Q],we=de-T[ie],ue=w?-W[se]/2:0,ce=L===pme?j[se]:W[se],ye=L===pme?-W[se]:-j[se],he=e.elements.arrow,pe=w&&he?iUt(he):{width:0,height:0},me=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:sui(),be=me[Q],xe=me[ie],Te=s4e(0,j[se],pe[se]),Ge=A?j[se]/2-ue-Te-be-Z.mainAxis:ce-Te-be-Z.mainAxis,tt=A?-j[se]/2+ue+Te+xe+Z.mainAxis:ye+Te+xe+Z.mainAxis,Ue=e.elements.arrow&&w5e(e.elements.arrow),Me=Ue?M==="y"?Ue.clientTop||0:Ue.clientLeft||0:0,He=(te=ee?.[M])!=null?te:0,at=de+Ge-He-Me,rt=de+tt-He,Be=s4e(w?sGe(ne,at):ne,de,w?wie(we,rt):we);F[M]=Be,G[M]=Be-de}if(c){var lt,ct=M==="x"?X4:Q4,ze=M==="x"?I3:A3,Ke=F[O],$e=O==="y"?"height":"width",nt=Ke+T[ct],vt=Ke-T[ze],Pt=[X4,Q4].indexOf(I)!==-1,Ct=(lt=ee?.[O])!=null?lt:0,Ye=Pt?nt:Ke-j[$e]-W[$e]-Ct+Z.altAxis,wt=Pt?Ke+j[$e]+W[$e]-Ct-Z.altAxis:vt,zt=w&&Pt?Xgr(Ye,Ke,wt):s4e(w?Ye:nt,Ke,w?wt:vt);F[O]=zt,G[O]=zt-Ke}e.modifiersData[i]=G}}const Lmr={name:"preventOverflow",enabled:!0,phase:"main",fn:Tmr,requiresIfExists:["offset"]};function Dmr(n){return{scrollLeft:n.scrollLeft,scrollTop:n.scrollTop}}function Imr(n){return n===Z6(n)||!y3(n)?sUt(n):Dmr(n)}function Amr(n){var e=n.getBoundingClientRect(),t=gme(e.width)/n.offsetWidth||1,i=gme(e.height)/n.offsetHeight||1;return t!==1||i!==1}function Rmr(n,e,t){t===void 0&&(t=!1);var i=y3(e),r=y3(e)&&Amr(e),o=PY(e),l=mme(n,r,t),c={scrollLeft:0,scrollTop:0},d={x:0,y:0};return(i||!i&&!t)&&(($9(e)!=="body"||aUt(o))&&(c=Imr(e)),y3(e)?(d=mme(e,!0),d.x+=e.clientLeft,d.y+=e.clientTop):o&&(d.x=oUt(o))),{x:l.left+c.scrollLeft-d.x,y:l.top+c.scrollTop-d.y,width:l.width,height:l.height}}function Mmr(n){var e=new Map,t=new Set,i=[];n.forEach(function(o){e.set(o.name,o)});function r(o){t.add(o.name);var l=[].concat(o.requires||[],o.requiresIfExists||[]);l.forEach(function(c){if(!t.has(c)){var d=e.get(c);d&&r(d)}}),i.push(o)}return n.forEach(function(o){t.has(o.name)||r(o)}),i}function Omr(n){var e=Mmr(n);return Ugr.reduce(function(t,i){return t.concat(e.filter(function(r){return r.phase===i}))},[])}function Nmr(n){var e;return function(){return e||(e=new Promise(function(t){Promise.resolve().then(function(){e=void 0,t(n())})})),e}}function Pmr(n){var e=n.reduce(function(t,i){var r=t[i.name];return t[i.name]=r?Object.assign({},r,i,{options:Object.assign({},r.options,i.options),data:Object.assign({},r.data,i.data)}):i,t},{});return Object.keys(e).map(function(t){return e[t]})}var TPn={placement:"bottom",modifiers:[],strategy:"absolute"};function LPn(){for(var n=arguments.length,e=new Array(n),t=0;t=19?n?.props?.ref||null:n?.ref||null}function Bmr(n){return typeof n=="function"?n():n}const uui=D.forwardRef(function(e,t){const{children:i,container:r,disablePortal:o=!1}=e,[l,c]=D.useState(null),d=xm(D.isValidElement(i)?FY(i):null,t);if(IS(()=>{o||c(Bmr(r)||document.body)},[r,o]),IS(()=>{if(l&&!o)return UOt(t,l),()=>{UOt(t,null)}},[t,l,o]),o){if(D.isValidElement(i)){const h={ref:d};return D.cloneElement(i,h)}return i}return l&&KR.createPortal(i,l)});function Wmr(n){return No("MuiPopper",n)}Po("MuiPopper",["root"]);function Vmr(n,e){if(e==="ltr")return n;switch(n){case"bottom-end":return"bottom-start";case"bottom-start":return"bottom-end";case"top-end":return"top-start";case"top-start":return"top-end";default:return n}}function QOt(n){return typeof n=="function"?n():n}function $mr(n){return n.nodeType!==void 0}const zmr=n=>{const{classes:e}=n;return Fo({root:["root"]},Wmr,e)},Umr={},qmr=D.forwardRef(function(e,t){const{anchorEl:i,children:r,direction:o,disablePortal:l,modifiers:c,open:d,placement:h,popperOptions:p,popperRef:m,slotProps:b={},slots:w={},TransitionProps:_,ownerState:x,...T}=e,I=D.useRef(null),L=xm(I,t),A=D.useRef(null),M=xm(A,m),O=D.useRef(M);IS(()=>{O.current=M},[M]),D.useImperativeHandle(m,()=>A.current,[]);const F=Vmr(h,o),[j,W]=D.useState(F),[q,Z]=D.useState(QOt(i));D.useEffect(()=>{A.current&&A.current.forceUpdate()}),D.useEffect(()=>{i&&Z(QOt(i))},[i]),IS(()=>{if(!q||!d)return;const ie=ne=>{W(ne.placement)};let se=[{name:"preventOverflow",options:{altBoundary:l}},{name:"flip",options:{altBoundary:l}},{name:"onUpdate",enabled:!0,phase:"afterWrite",fn:({state:ne})=>{ie(ne)}}];c!=null&&(se=se.concat(c)),p&&p.modifiers!=null&&(se=se.concat(p.modifiers));const de=Hmr(q,I.current,{placement:F,...p,modifiers:se});return O.current(de),()=>{de.destroy(),O.current(null)}},[q,l,c,d,p,F]);const ee={placement:j};_!==null&&(ee.TransitionProps=_);const G=zmr(e),te=w.root??"div",Q=dE({elementType:te,externalSlotProps:b.root,externalForwardedProps:T,additionalProps:{role:"tooltip",ref:L},ownerState:e,className:G.root});return k.jsx(te,{...Q,children:typeof r=="function"?r(ee):r})}),Gmr=D.forwardRef(function(e,t){const{anchorEl:i,children:r,container:o,direction:l="ltr",disablePortal:c=!1,keepMounted:d=!1,modifiers:h,open:p,placement:m="bottom",popperOptions:b=Umr,popperRef:w,style:_,transition:x=!1,slotProps:T={},slots:I={},...L}=e,[A,M]=D.useState(!0),O=()=>{M(!1)},F=()=>{M(!0)};if(!d&&!p&&(!x||A))return null;let j;if(o)j=o;else if(i){const Z=QOt(i);j=Z&&$mr(Z)?hv(Z).body:hv(null).body}const W=!p&&d&&(!x||A)?"none":void 0,q=x?{in:p,onEnter:O,onExited:F}:void 0;return k.jsx(uui,{disablePortal:c,container:j,children:k.jsx(qmr,{anchorEl:i,direction:l,disablePortal:c,modifiers:h,ref:t,open:x?!A:p,placement:m,popperOptions:b,popperRef:w,slotProps:T,slots:I,...L,style:{position:"fixed",top:0,left:0,display:W,..._},TransitionProps:q,children:r})})}),Kmr=tn(Gmr,{name:"MuiPopper",slot:"Root"})({}),c8=D.forwardRef(function(e,t){const i=MY(),r=Wo({props:e,name:"MuiPopper"}),{anchorEl:o,component:l,components:c,componentsProps:d,container:h,disablePortal:p,keepMounted:m,modifiers:b,open:w,placement:_,popperOptions:x,popperRef:T,transition:I,slots:L,slotProps:A,...M}=r,O=L?.root??c?.Root,F={anchorEl:o,container:h,disablePortal:p,keepMounted:m,modifiers:b,open:w,placement:_,popperOptions:x,popperRef:T,transition:I,...M};return k.jsx(Kmr,{as:l,direction:i?"rtl":"ltr",slots:{root:O},slotProps:A??d,...F,ref:t})});function Ymr(n){return No("MuiListSubheader",n)}Po("MuiListSubheader",["root","colorPrimary","colorInherit","gutters","inset","sticky"]);const Zmr=n=>{const{classes:e,color:t,disableGutters:i,inset:r,disableSticky:o}=n,l={root:["root",t!=="default"&&`color${ii(t)}`,!i&&"gutters",r&&"inset",!o&&"sticky"]};return Fo(l,Ymr,e)},Xmr=tn("li",{name:"MuiListSubheader",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.color!=="default"&&e[`color${ii(t.color)}`],!t.disableGutters&&e.gutters,t.inset&&e.inset,!t.disableSticky&&e.sticky]}})(Gs(({theme:n})=>({boxSizing:"border-box",lineHeight:"48px",listStyle:"none",color:(n.vars||n).palette.text.secondary,fontFamily:n.typography.fontFamily,fontWeight:n.typography.fontWeightMedium,fontSize:n.typography.pxToRem(14),variants:[{props:{color:"primary"},style:{color:(n.vars||n).palette.primary.main}},{props:{color:"inherit"},style:{color:"inherit"}},{props:({ownerState:e})=>!e.disableGutters,style:{paddingLeft:16,paddingRight:16}},{props:({ownerState:e})=>e.inset,style:{paddingLeft:72}},{props:({ownerState:e})=>!e.disableSticky,style:{position:"sticky",top:0,zIndex:1,backgroundColor:(n.vars||n).palette.background.paper}}]}))),JOt=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiListSubheader"}),{className:r,color:o="default",component:l="li",disableGutters:c=!1,disableSticky:d=!1,inset:h=!1,...p}=i,m={...i,color:o,component:l,disableGutters:c,disableSticky:d,inset:h},b=Zmr(m);return k.jsx(Xmr,{as:l,className:_i(b.root,r),ref:t,ownerState:m,...p})});JOt&&(JOt.muiSkipListHighlight=!0);const Qmr=Ya(k.jsx("path",{d:"M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"}));function Jmr(n){return No("MuiChip",n)}const jd=Po("MuiChip",["root","sizeSmall","sizeMedium","colorDefault","colorError","colorInfo","colorPrimary","colorSecondary","colorSuccess","colorWarning","disabled","clickable","clickableColorPrimary","clickableColorSecondary","deletable","deletableColorPrimary","deletableColorSecondary","outlined","filled","outlinedPrimary","outlinedSecondary","filledPrimary","filledSecondary","avatar","avatarSmall","avatarMedium","avatarColorPrimary","avatarColorSecondary","icon","iconSmall","iconMedium","iconColorPrimary","iconColorSecondary","label","labelSmall","labelMedium","deleteIcon","deleteIconSmall","deleteIconMedium","deleteIconColorPrimary","deleteIconColorSecondary","deleteIconOutlinedColorPrimary","deleteIconOutlinedColorSecondary","deleteIconFilledColorPrimary","deleteIconFilledColorSecondary","focusVisible"]),e1r=n=>{const{classes:e,disabled:t,size:i,color:r,iconColor:o,onDelete:l,clickable:c,variant:d}=n,h={root:["root",d,t&&"disabled",`size${ii(i)}`,`color${ii(r)}`,c&&"clickable",c&&`clickableColor${ii(r)}`,l&&"deletable",l&&`deletableColor${ii(r)}`,`${d}${ii(r)}`],label:["label",`label${ii(i)}`],avatar:["avatar",`avatar${ii(i)}`,`avatarColor${ii(r)}`],icon:["icon",`icon${ii(i)}`,`iconColor${ii(o)}`],deleteIcon:["deleteIcon",`deleteIcon${ii(i)}`,`deleteIconColor${ii(r)}`,`deleteIcon${ii(d)}Color${ii(r)}`]};return Fo(h,Jmr,e)},t1r=tn("div",{name:"MuiChip",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n,{color:i,iconColor:r,clickable:o,onDelete:l,size:c,variant:d}=t;return[{[`& .${jd.avatar}`]:e.avatar},{[`& .${jd.avatar}`]:e[`avatar${ii(c)}`]},{[`& .${jd.avatar}`]:e[`avatarColor${ii(i)}`]},{[`& .${jd.icon}`]:e.icon},{[`& .${jd.icon}`]:e[`icon${ii(c)}`]},{[`& .${jd.icon}`]:e[`iconColor${ii(r)}`]},{[`& .${jd.deleteIcon}`]:e.deleteIcon},{[`& .${jd.deleteIcon}`]:e[`deleteIcon${ii(c)}`]},{[`& .${jd.deleteIcon}`]:e[`deleteIconColor${ii(i)}`]},{[`& .${jd.deleteIcon}`]:e[`deleteIcon${ii(d)}Color${ii(i)}`]},e.root,e[`size${ii(c)}`],e[`color${ii(i)}`],o&&e.clickable,o&&i!=="default"&&e[`clickableColor${ii(i)}`],l&&e.deletable,l&&i!=="default"&&e[`deletableColor${ii(i)}`],e[d],e[`${d}${ii(i)}`]]}})(Gs(({theme:n})=>{const e=n.palette.mode==="light"?n.palette.grey[700]:n.palette.grey[300];return{maxWidth:"100%",fontFamily:n.typography.fontFamily,fontSize:n.typography.pxToRem(13),display:"inline-flex",alignItems:"center",justifyContent:"center",height:32,lineHeight:1.5,color:(n.vars||n).palette.text.primary,backgroundColor:(n.vars||n).palette.action.selected,borderRadius:32/2,whiteSpace:"nowrap",transition:n.transitions.create(["background-color","box-shadow"]),cursor:"unset",outline:0,textDecoration:"none",border:0,padding:0,verticalAlign:"middle",boxSizing:"border-box",[`&.${jd.disabled}`]:{opacity:(n.vars||n).palette.action.disabledOpacity,pointerEvents:"none"},[`& .${jd.avatar}`]:{marginLeft:5,marginRight:-6,width:24,height:24,color:n.vars?n.vars.palette.Chip.defaultAvatarColor:e,fontSize:n.typography.pxToRem(12)},[`& .${jd.avatarColorPrimary}`]:{color:(n.vars||n).palette.primary.contrastText,backgroundColor:(n.vars||n).palette.primary.dark},[`& .${jd.avatarColorSecondary}`]:{color:(n.vars||n).palette.secondary.contrastText,backgroundColor:(n.vars||n).palette.secondary.dark},[`& .${jd.avatarSmall}`]:{marginLeft:4,marginRight:-4,width:18,height:18,fontSize:n.typography.pxToRem(10)},[`& .${jd.icon}`]:{marginLeft:5,marginRight:-6},[`& .${jd.deleteIcon}`]:{WebkitTapHighlightColor:"transparent",color:n.alpha((n.vars||n).palette.text.primary,.26),fontSize:22,cursor:"pointer",margin:"0 5px 0 -6px","&:hover":{color:n.alpha((n.vars||n).palette.text.primary,.4)}},variants:[{props:{size:"small"},style:{height:24,[`& .${jd.icon}`]:{fontSize:18,marginLeft:4,marginRight:-4},[`& .${jd.deleteIcon}`]:{fontSize:16,marginRight:4,marginLeft:-4}}},...Object.entries(n.palette).filter(Vh(["contrastText"])).map(([t])=>({props:{color:t},style:{backgroundColor:(n.vars||n).palette[t].main,color:(n.vars||n).palette[t].contrastText,[`& .${jd.deleteIcon}`]:{color:n.alpha((n.vars||n).palette[t].contrastText,.7),"&:hover, &:active":{color:(n.vars||n).palette[t].contrastText}}}})),{props:t=>t.iconColor===t.color,style:{[`& .${jd.icon}`]:{color:n.vars?n.vars.palette.Chip.defaultIconColor:e}}},{props:t=>t.iconColor===t.color&&t.color!=="default",style:{[`& .${jd.icon}`]:{color:"inherit"}}},{props:{onDelete:!0},style:{[`&.${jd.focusVisible}`]:{backgroundColor:n.alpha((n.vars||n).palette.action.selected,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.focusOpacity}`)}}},...Object.entries(n.palette).filter(Vh(["dark"])).map(([t])=>({props:{color:t,onDelete:!0},style:{[`&.${jd.focusVisible}`]:{background:(n.vars||n).palette[t].dark}}})),{props:{clickable:!0},style:{userSelect:"none",WebkitTapHighlightColor:"transparent",cursor:"pointer","&:hover":{backgroundColor:n.alpha((n.vars||n).palette.action.selected,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.hoverOpacity}`)},[`&.${jd.focusVisible}`]:{backgroundColor:n.alpha((n.vars||n).palette.action.selected,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.focusOpacity}`)},"&:active":{boxShadow:(n.vars||n).shadows[1]}}},...Object.entries(n.palette).filter(Vh(["dark"])).map(([t])=>({props:{color:t,clickable:!0},style:{[`&:hover, &.${jd.focusVisible}`]:{backgroundColor:(n.vars||n).palette[t].dark}}})),{props:{variant:"outlined"},style:{backgroundColor:"transparent",border:n.vars?`1px solid ${n.vars.palette.Chip.defaultBorder}`:`1px solid ${n.palette.mode==="light"?n.palette.grey[400]:n.palette.grey[700]}`,[`&.${jd.clickable}:hover`]:{backgroundColor:(n.vars||n).palette.action.hover},[`&.${jd.focusVisible}`]:{backgroundColor:(n.vars||n).palette.action.focus},[`& .${jd.avatar}`]:{marginLeft:4},[`& .${jd.avatarSmall}`]:{marginLeft:2},[`& .${jd.icon}`]:{marginLeft:4},[`& .${jd.iconSmall}`]:{marginLeft:2},[`& .${jd.deleteIcon}`]:{marginRight:5},[`& .${jd.deleteIconSmall}`]:{marginRight:3}}},...Object.entries(n.palette).filter(Vh()).map(([t])=>({props:{variant:"outlined",color:t},style:{color:(n.vars||n).palette[t].main,border:`1px solid ${n.alpha((n.vars||n).palette[t].main,.7)}`,[`&.${jd.clickable}:hover`]:{backgroundColor:n.alpha((n.vars||n).palette[t].main,(n.vars||n).palette.action.hoverOpacity)},[`&.${jd.focusVisible}`]:{backgroundColor:n.alpha((n.vars||n).palette[t].main,(n.vars||n).palette.action.focusOpacity)},[`& .${jd.deleteIcon}`]:{color:n.alpha((n.vars||n).palette[t].main,.7),"&:hover, &:active":{color:(n.vars||n).palette[t].main}}}}))]}})),n1r=tn("span",{name:"MuiChip",slot:"Label",overridesResolver:(n,e)=>{const{ownerState:t}=n,{size:i}=t;return[e.label,e[`label${ii(i)}`]]}})({overflow:"hidden",textOverflow:"ellipsis",paddingLeft:12,paddingRight:12,whiteSpace:"nowrap",variants:[{props:{variant:"outlined"},style:{paddingLeft:11,paddingRight:11}},{props:{size:"small"},style:{paddingLeft:8,paddingRight:8}},{props:{size:"small",variant:"outlined"},style:{paddingLeft:7,paddingRight:7}}]});function DPn(n){return n.key==="Backspace"||n.key==="Delete"}const N_=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiChip"}),{avatar:r,className:o,clickable:l,color:c="default",component:d,deleteIcon:h,disabled:p=!1,icon:m,label:b,onClick:w,onDelete:_,onKeyDown:x,onKeyUp:T,size:I="medium",variant:L="filled",tabIndex:A,skipFocusWhenDisabled:M=!1,slots:O={},slotProps:F={},...j}=i,W=D.useRef(null),q=xm(W,t),Z=be=>{be.stopPropagation(),_(be)},ee=be=>{be.currentTarget===be.target&&DPn(be)&&be.preventDefault(),x&&x(be)},G=be=>{be.currentTarget===be.target&&_&&DPn(be)&&_(be),T&&T(be)},te=l!==!1&&w?!0:l,Q=te||_?D3:d||"div",ie={...i,component:Q,disabled:p,size:I,color:c,iconColor:D.isValidElement(m)&&m.props.color||c,onDelete:!!_,clickable:te,variant:L},se=e1r(ie),de=Q===D3?{component:d||"div",focusVisibleClassName:se.focusVisible,..._&&{disableRipple:!0}}:{};let ne=null;_&&(ne=h&&D.isValidElement(h)?D.cloneElement(h,{className:_i(h.props.className,se.deleteIcon),onClick:Z}):k.jsx(Qmr,{className:se.deleteIcon,onClick:Z}));let we=null;r&&D.isValidElement(r)&&(we=D.cloneElement(r,{className:_i(se.avatar,r.props.className)}));let ue=null;m&&D.isValidElement(m)&&(ue=D.cloneElement(m,{className:_i(se.icon,m.props.className)}));const ce={slots:O,slotProps:F},[ye,he]=_o("root",{elementType:t1r,externalForwardedProps:{...ce,...j},ownerState:ie,shouldForwardComponentProp:!0,ref:q,className:_i(se.root,o),additionalProps:{disabled:te&&p?!0:void 0,tabIndex:M&&p?-1:A,...de},getSlotProps:be=>({...be,onClick:xe=>{be.onClick?.(xe),w?.(xe)},onKeyDown:xe=>{be.onKeyDown?.(xe),ee(xe)},onKeyUp:xe=>{be.onKeyUp?.(xe),G(xe)}})}),[pe,me]=_o("label",{elementType:n1r,externalForwardedProps:ce,ownerState:ie,className:se.label});return k.jsxs(ye,{as:Q,...he,children:[we||ue,k.jsx(pe,{...me,children:b}),ne]})});function OBe(n){return parseInt(n,10)||0}const i1r={shadow:{visibility:"hidden",position:"absolute",overflow:"hidden",height:0,top:0,left:0,transform:"translateZ(0)"}};function r1r(n){for(const e in n)return!1;return!0}function IPn(n){return r1r(n)||n.outerHeightStyle===0&&!n.overflowing}const s1r=D.forwardRef(function(e,t){const{onChange:i,maxRows:r,minRows:o=1,style:l,value:c,...d}=e,{current:h}=D.useRef(c!=null),p=D.useRef(null),m=xm(t,p),b=D.useRef(null),w=D.useRef(null),_=D.useCallback(()=>{const A=p.current,M=w.current;if(!A||!M)return;const F=Y6(A).getComputedStyle(A);if(F.width==="0px")return{outerHeightStyle:0,overflowing:!1};M.style.width=F.width,M.value=A.value||e.placeholder||"x",M.value.slice(-1)===` -`&&(M.value+=" ");const j=F.boxSizing,W=OBe(F.paddingBottom)+OBe(F.paddingTop),q=OBe(F.borderBottomWidth)+OBe(F.borderTopWidth),Z=M.scrollHeight;M.value="x";const ee=M.scrollHeight;let G=Z;o&&(G=Math.max(Number(o)*ee,G)),r&&(G=Math.min(Number(r)*ee,G)),G=Math.max(G,ee);const te=G+(j==="border-box"?W+q:0),Q=Math.abs(G-Z)<=1;return{outerHeightStyle:te,overflowing:Q}},[r,o,e.placeholder]),x=ub(()=>{const A=p.current,M=_();if(!A||!M||IPn(M))return!1;const O=M.outerHeightStyle;return b.current!=null&&b.current!==O}),T=D.useCallback(()=>{const A=p.current,M=_();if(!A||!M||IPn(M))return;const O=M.outerHeightStyle;b.current!==O&&(b.current=O,A.style.height=`${O}px`),A.style.overflow=M.overflowing?"hidden":""},[_]),I=D.useRef(-1);IS(()=>{const A=g5e(T),M=p?.current;if(!M)return;const O=Y6(M);O.addEventListener("resize",A);let F;return typeof ResizeObserver<"u"&&(F=new ResizeObserver(()=>{x()&&(F.unobserve(M),cancelAnimationFrame(I.current),T(),I.current=requestAnimationFrame(()=>{F.observe(M)}))}),F.observe(M)),()=>{A.clear(),cancelAnimationFrame(I.current),O.removeEventListener("resize",A),F&&F.disconnect()}},[_,T,x]),IS(()=>{T()});const L=A=>{h||T();const M=A.target,O=M.value.length,F=M.value.endsWith(` -`),j=M.selectionStart===O;F&&j&&M.setSelectionRange(O,O),i&&i(A)};return k.jsxs(D.Fragment,{children:[k.jsx("textarea",{value:c,onChange:L,ref:m,rows:o,style:l,...d}),k.jsx("textarea",{"aria-hidden":!0,className:e.className,readOnly:!0,ref:w,tabIndex:-1,style:{...i1r.shadow,...l,paddingTop:0,paddingBottom:0}})]})});function jY({props:n,states:e,muiFormControl:t}){return e.reduce((i,r)=>(i[r]=n[r],t&&typeof n[r]>"u"&&(i[r]=t[r]),i),{})}const $et=D.createContext(void 0);function DM(){return D.useContext($et)}function APn(n){return n!=null&&!(Array.isArray(n)&&n.length===0)}function oGe(n,e=!1){return n&&(APn(n.value)&&n.value!==""||e&&APn(n.defaultValue)&&n.defaultValue!=="")}function o1r(n){return n.startAdornment}function a1r(n){return No("MuiInputBase",n)}const b6=Po("MuiInputBase",["root","formControl","focused","disabled","adornedStart","adornedEnd","error","sizeSmall","multiline","colorSecondary","fullWidth","hiddenLabel","readOnly","input","inputSizeSmall","inputMultiline","inputTypeSearch","inputAdornedStart","inputAdornedEnd","inputHiddenLabel"]);var RPn;const zet=(n,e)=>{const{ownerState:t}=n;return[e.root,t.formControl&&e.formControl,t.startAdornment&&e.adornedStart,t.endAdornment&&e.adornedEnd,t.error&&e.error,t.size==="small"&&e.sizeSmall,t.multiline&&e.multiline,t.color&&e[`color${ii(t.color)}`],t.fullWidth&&e.fullWidth,t.hiddenLabel&&e.hiddenLabel]},Uet=(n,e)=>{const{ownerState:t}=n;return[e.input,t.size==="small"&&e.inputSizeSmall,t.multiline&&e.inputMultiline,t.type==="search"&&e.inputTypeSearch,t.startAdornment&&e.inputAdornedStart,t.endAdornment&&e.inputAdornedEnd,t.hiddenLabel&&e.inputHiddenLabel]},l1r=n=>{const{classes:e,color:t,disabled:i,error:r,endAdornment:o,focused:l,formControl:c,fullWidth:d,hiddenLabel:h,multiline:p,readOnly:m,size:b,startAdornment:w,type:_}=n,x={root:["root",`color${ii(t)}`,i&&"disabled",r&&"error",d&&"fullWidth",l&&"focused",c&&"formControl",b&&b!=="medium"&&`size${ii(b)}`,p&&"multiline",w&&"adornedStart",o&&"adornedEnd",h&&"hiddenLabel",m&&"readOnly"],input:["input",i&&"disabled",_==="search"&&"inputTypeSearch",p&&"inputMultiline",b==="small"&&"inputSizeSmall",h&&"inputHiddenLabel",w&&"inputAdornedStart",o&&"inputAdornedEnd",m&&"readOnly"]};return Fo(x,a1r,e)},qet=tn("div",{name:"MuiInputBase",slot:"Root",overridesResolver:zet})(Gs(({theme:n})=>({...n.typography.body1,color:(n.vars||n).palette.text.primary,lineHeight:"1.4375em",boxSizing:"border-box",position:"relative",cursor:"text",display:"inline-flex",alignItems:"center",[`&.${b6.disabled}`]:{color:(n.vars||n).palette.text.disabled,cursor:"default"},variants:[{props:({ownerState:e})=>e.multiline,style:{padding:"4px 0 5px"}},{props:({ownerState:e,size:t})=>e.multiline&&t==="small",style:{paddingTop:1}},{props:({ownerState:e})=>e.fullWidth,style:{width:"100%"}}]}))),Get=tn("input",{name:"MuiInputBase",slot:"Input",overridesResolver:Uet})(Gs(({theme:n})=>{const e=n.palette.mode==="light",t={color:"currentColor",...n.vars?{opacity:n.vars.opacity.inputPlaceholder}:{opacity:e?.42:.5},transition:n.transitions.create("opacity",{duration:n.transitions.duration.shorter})},i={opacity:"0 !important"},r=n.vars?{opacity:n.vars.opacity.inputPlaceholder}:{opacity:e?.42:.5};return{font:"inherit",letterSpacing:"inherit",color:"currentColor",padding:"4px 0 5px",border:0,boxSizing:"content-box",background:"none",height:"1.4375em",margin:0,WebkitTapHighlightColor:"transparent",display:"block",minWidth:0,width:"100%","&::-webkit-input-placeholder":t,"&::-moz-placeholder":t,"&::-ms-input-placeholder":t,"&:focus":{outline:0},"&:invalid":{boxShadow:"none"},"&::-webkit-search-decoration":{WebkitAppearance:"none"},[`label[data-shrink=false] + .${b6.formControl} &`]:{"&::-webkit-input-placeholder":i,"&::-moz-placeholder":i,"&::-ms-input-placeholder":i,"&:focus::-webkit-input-placeholder":r,"&:focus::-moz-placeholder":r,"&:focus::-ms-input-placeholder":r},[`&.${b6.disabled}`]:{opacity:1,WebkitTextFillColor:(n.vars||n).palette.text.disabled},variants:[{props:({ownerState:o})=>!o.disableInjectingGlobalStyles,style:{animationName:"mui-auto-fill-cancel",animationDuration:"10ms","&:-webkit-autofill":{animationDuration:"5000s",animationName:"mui-auto-fill"}}},{props:{size:"small"},style:{paddingTop:1}},{props:({ownerState:o})=>o.multiline,style:{height:"auto",resize:"none",padding:0,paddingTop:0}},{props:{type:"search"},style:{MozAppearance:"textfield"}}]}})),MPn=Fzt({"@keyframes mui-auto-fill":{from:{display:"block"}},"@keyframes mui-auto-fill-cancel":{from:{display:"block"}}}),Y1e=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiInputBase"}),{"aria-describedby":r,autoComplete:o,autoFocus:l,className:c,color:d,components:h={},componentsProps:p={},defaultValue:m,disabled:b,disableInjectingGlobalStyles:w,endAdornment:_,error:x,fullWidth:T=!1,id:I,inputComponent:L="input",inputProps:A={},inputRef:M,margin:O,maxRows:F,minRows:j,multiline:W=!1,name:q,onBlur:Z,onChange:ee,onClick:G,onFocus:te,onKeyDown:Q,onKeyUp:ie,placeholder:se,readOnly:de,renderSuffix:ne,rows:we,size:ue,slotProps:ce={},slots:ye={},startAdornment:he,type:pe="text",value:me,...be}=i,xe=A.value!=null?A.value:me,{current:Te}=D.useRef(xe!=null),Ge=D.useRef(),tt=D.useCallback(hn=>{},[]),Ue=xm(Ge,M,A.ref,tt),[Me,He]=D.useState(!1),at=DM(),rt=jY({props:i,muiFormControl:at,states:["color","disabled","error","hiddenLabel","size","required","filled"]});rt.focused=at?at.focused:Me,D.useEffect(()=>{!at&&b&&Me&&(He(!1),Z&&Z())},[at,b,Me,Z]);const Be=at&&at.onFilled,lt=at&&at.onEmpty,ct=D.useCallback(hn=>{oGe(hn)?Be&&Be():lt&<()},[Be,lt]);IS(()=>{Te&&ct({value:xe})},[xe,ct,Te]);const ze=hn=>{te&&te(hn),A.onFocus&&A.onFocus(hn),at&&at.onFocus?at.onFocus(hn):He(!0)},Ke=hn=>{Z&&Z(hn),A.onBlur&&A.onBlur(hn),at&&at.onBlur?at.onBlur(hn):He(!1)},$e=(hn,...Zi)=>{if(!Te){const $i=hn.target||Ge.current;if($i==null)throw new Error(bW(1));ct({value:$i.value})}A.onChange&&A.onChange(hn,...Zi),ee&&ee(hn,...Zi)};D.useEffect(()=>{ct(Ge.current)},[]);const nt=hn=>{Ge.current&&hn.currentTarget===hn.target&&Ge.current.focus(),G&&G(hn)};let vt=L,Pt=A;W&&vt==="input"&&(we?Pt={type:void 0,minRows:we,maxRows:we,...Pt}:Pt={type:void 0,maxRows:F,minRows:j,...Pt},vt=s1r);const Ct=hn=>{ct(hn.animationName==="mui-auto-fill-cancel"?Ge.current:{value:"x"})};D.useEffect(()=>{at&&at.setAdornedStart(!!he)},[at,he]);const Ye={...i,color:rt.color||"primary",disabled:rt.disabled,endAdornment:_,error:rt.error,focused:rt.focused,formControl:at,fullWidth:T,hiddenLabel:rt.hiddenLabel,multiline:W,size:rt.size,startAdornment:he,type:pe},wt=l1r(Ye),zt=ye.root||h.Root||qet,mn=ce.root||p.root||{},xn=ye.input||h.Input||Get;return Pt={...Pt,...ce.input??p.input},k.jsxs(D.Fragment,{children:[!w&&typeof MPn=="function"&&(RPn||(RPn=k.jsx(MPn,{}))),k.jsxs(zt,{...mn,ref:t,onClick:nt,...be,...!x9(zt)&&{ownerState:{...Ye,...mn.ownerState}},className:_i(wt.root,mn.className,c,de&&"MuiInputBase-readOnly"),children:[he,k.jsx($et.Provider,{value:null,children:k.jsx(xn,{"aria-invalid":rt.error,"aria-describedby":r,autoComplete:o,autoFocus:l,defaultValue:m,disabled:rt.disabled,id:I,onAnimationStart:Ct,name:q,placeholder:se,readOnly:de,required:rt.required,rows:we,value:xe,onKeyDown:Q,onKeyUp:ie,type:pe,...Pt,...!x9(xn)&&{as:vt,ownerState:{...Ye,...Pt.ownerState}},ref:Ue,className:_i(wt.input,Pt.className,de&&"MuiInputBase-readOnly"),onBlur:Ke,onChange:$e,onFocus:ze})}),_,ne?ne({...rt,startAdornment:he}):null]})]})});function c1r(n){return No("MuiInput",n)}const yG={...b6,...Po("MuiInput",["root","underline","input"])};function u1r(n){return No("MuiOutlinedInput",n)}const zD={...b6,...Po("MuiOutlinedInput",["root","notchedOutline","input"])};function d1r(n){return No("MuiFilledInput",n)}const v6={...b6,...Po("MuiFilledInput",["root","underline","input","adornedStart","adornedEnd","sizeSmall","multiline","hiddenLabel"])},dui=Ya(k.jsx("path",{d:"M7 10l5 5 5-5z"}));function h1r(n){return No("MuiAutocomplete",n)}const qu=Po("MuiAutocomplete",["root","expanded","fullWidth","focused","focusVisible","tag","tagSizeSmall","tagSizeMedium","hasPopupIcon","hasClearIcon","inputRoot","input","inputFocused","endAdornment","clearIndicator","popupIndicator","popupIndicatorOpen","popper","popperDisablePortal","paper","listbox","loading","noOptions","option","groupLabel","groupUl"]);var OPn,NPn;const f1r=n=>{const{classes:e,disablePortal:t,expanded:i,focused:r,fullWidth:o,hasClearIcon:l,hasPopupIcon:c,inputFocused:d,popupOpen:h,size:p}=n,m={root:["root",i&&"expanded",r&&"focused",o&&"fullWidth",l&&"hasClearIcon",c&&"hasPopupIcon"],inputRoot:["inputRoot"],input:["input",d&&"inputFocused"],tag:["tag",`tagSize${ii(p)}`],endAdornment:["endAdornment"],clearIndicator:["clearIndicator"],popupIndicator:["popupIndicator",h&&"popupIndicatorOpen"],popper:["popper",t&&"popperDisablePortal"],paper:["paper"],listbox:["listbox"],loading:["loading"],noOptions:["noOptions"],option:["option"],groupLabel:["groupLabel"],groupUl:["groupUl"]};return Fo(m,h1r,e)},p1r=tn("div",{name:"MuiAutocomplete",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n,{fullWidth:i,hasClearIcon:r,hasPopupIcon:o,inputFocused:l,size:c}=t;return[{[`& .${qu.tag}`]:e.tag},{[`& .${qu.tag}`]:e[`tagSize${ii(c)}`]},{[`& .${qu.inputRoot}`]:e.inputRoot},{[`& .${qu.input}`]:e.input},{[`& .${qu.input}`]:l&&e.inputFocused},e.root,i&&e.fullWidth,o&&e.hasPopupIcon,r&&e.hasClearIcon]}})({[`&.${qu.focused} .${qu.clearIndicator}`]:{visibility:"visible"},"@media (pointer: fine)":{[`&:hover .${qu.clearIndicator}`]:{visibility:"visible"}},[`& .${qu.tag}`]:{margin:3,maxWidth:"calc(100% - 6px)"},[`& .${qu.inputRoot}`]:{[`.${qu.hasPopupIcon}&, .${qu.hasClearIcon}&`]:{paddingRight:30},[`.${qu.hasPopupIcon}.${qu.hasClearIcon}&`]:{paddingRight:56},[`& .${qu.input}`]:{width:0,minWidth:30}},[`& .${yG.root}`]:{paddingBottom:1,"& .MuiInput-input":{padding:"4px 4px 4px 0px"}},[`& .${yG.root}.${b6.sizeSmall}`]:{[`& .${yG.input}`]:{padding:"2px 4px 3px 0"}},[`& .${zD.root}`]:{padding:9,[`.${qu.hasPopupIcon}&, .${qu.hasClearIcon}&`]:{paddingRight:39},[`.${qu.hasPopupIcon}.${qu.hasClearIcon}&`]:{paddingRight:65},[`& .${qu.input}`]:{padding:"7.5px 4px 7.5px 5px"},[`& .${qu.endAdornment}`]:{right:9}},[`& .${zD.root}.${b6.sizeSmall}`]:{paddingTop:6,paddingBottom:6,paddingLeft:6,[`& .${qu.input}`]:{padding:"2.5px 4px 2.5px 8px"}},[`& .${v6.root}`]:{paddingTop:19,paddingLeft:8,[`.${qu.hasPopupIcon}&, .${qu.hasClearIcon}&`]:{paddingRight:39},[`.${qu.hasPopupIcon}.${qu.hasClearIcon}&`]:{paddingRight:65},[`& .${v6.input}`]:{padding:"7px 4px"},[`& .${qu.endAdornment}`]:{right:9}},[`& .${v6.root}.${b6.sizeSmall}`]:{paddingBottom:1,[`& .${v6.input}`]:{padding:"2.5px 4px"}},[`& .${b6.hiddenLabel}`]:{paddingTop:8},[`& .${v6.root}.${b6.hiddenLabel}`]:{paddingTop:0,paddingBottom:0,[`& .${qu.input}`]:{paddingTop:16,paddingBottom:17}},[`& .${v6.root}.${b6.hiddenLabel}.${b6.sizeSmall}`]:{[`& .${qu.input}`]:{paddingTop:8,paddingBottom:9}},[`& .${qu.input}`]:{flexGrow:1,textOverflow:"ellipsis",opacity:0},variants:[{props:{fullWidth:!0},style:{width:"100%"}},{props:{size:"small"},style:{[`& .${qu.tag}`]:{margin:2,maxWidth:"calc(100% - 4px)"}}},{props:{inputFocused:!0},style:{[`& .${qu.input}`]:{opacity:1}}},{props:{multiple:!0},style:{[`& .${qu.inputRoot}`]:{flexWrap:"wrap"}}}]}),g1r=tn("div",{name:"MuiAutocomplete",slot:"EndAdornment"})({position:"absolute",right:0,top:"50%",transform:"translate(0, -50%)"}),m1r=tn(da,{name:"MuiAutocomplete",slot:"ClearIndicator"})({marginRight:-2,padding:4,visibility:"hidden"}),b1r=tn(da,{name:"MuiAutocomplete",slot:"PopupIndicator",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.popupIndicator,t.popupOpen&&e.popupIndicatorOpen]}})({padding:2,marginRight:-2,variants:[{props:{popupOpen:!0},style:{transform:"rotate(180deg)"}}]}),v1r=tn(c8,{name:"MuiAutocomplete",slot:"Popper",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[{[`& .${qu.option}`]:e.option},e.popper,t.disablePortal&&e.popperDisablePortal]}})(Gs(({theme:n})=>({zIndex:(n.vars||n).zIndex.modal,variants:[{props:{disablePortal:!0},style:{position:"absolute"}}]}))),w1r=tn(Jf,{name:"MuiAutocomplete",slot:"Paper"})(Gs(({theme:n})=>({...n.typography.body1,overflow:"auto"}))),y1r=tn("div",{name:"MuiAutocomplete",slot:"Loading"})(Gs(({theme:n})=>({color:(n.vars||n).palette.text.secondary,padding:"14px 16px"}))),_1r=tn("div",{name:"MuiAutocomplete",slot:"NoOptions"})(Gs(({theme:n})=>({color:(n.vars||n).palette.text.secondary,padding:"14px 16px"}))),C1r=tn("ul",{name:"MuiAutocomplete",slot:"Listbox"})(Gs(({theme:n})=>({listStyle:"none",margin:0,padding:"8px 0",maxHeight:"40vh",overflow:"auto",position:"relative",[`& .${qu.option}`]:{minHeight:48,display:"flex",overflow:"hidden",justifyContent:"flex-start",alignItems:"center",cursor:"pointer",paddingTop:6,boxSizing:"border-box",outline:"0",WebkitTapHighlightColor:"transparent",paddingBottom:6,paddingLeft:16,paddingRight:16,[n.breakpoints.up("sm")]:{minHeight:"auto"},[`&.${qu.focused}`]:{backgroundColor:(n.vars||n).palette.action.hover,"@media (hover: none)":{backgroundColor:"transparent"}},'&[aria-disabled="true"]':{opacity:(n.vars||n).palette.action.disabledOpacity,pointerEvents:"none"},[`&.${qu.focusVisible}`]:{backgroundColor:(n.vars||n).palette.action.focus},'&[aria-selected="true"]':{backgroundColor:n.alpha((n.vars||n).palette.primary.main,(n.vars||n).palette.action.selectedOpacity),[`&.${qu.focused}`]:{backgroundColor:n.alpha((n.vars||n).palette.primary.main,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.hoverOpacity}`),"@media (hover: none)":{backgroundColor:(n.vars||n).palette.action.selected}},[`&.${qu.focusVisible}`]:{backgroundColor:n.alpha((n.vars||n).palette.primary.main,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.focusOpacity}`)}}}}))),S1r=tn(JOt,{name:"MuiAutocomplete",slot:"GroupLabel"})(Gs(({theme:n})=>({backgroundColor:(n.vars||n).palette.background.paper,top:-8}))),x1r=tn("ul",{name:"MuiAutocomplete",slot:"GroupUl"})({padding:0,[`& .${qu.option}`]:{paddingLeft:24}}),Ket=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiAutocomplete"}),{autoComplete:r=!1,autoHighlight:o=!1,autoSelect:l=!1,blurOnSelect:c=!1,ChipProps:d,className:h,clearIcon:p=OPn||(OPn=k.jsx(eui,{fontSize:"small"})),clearOnBlur:m=!i.freeSolo,clearOnEscape:b=!1,clearText:w="Clear",closeText:_="Close",componentsProps:x,defaultValue:T=i.multiple?[]:null,disableClearable:I=!1,disableCloseOnSelect:L=!1,disabled:A=!1,disabledItemsFocusable:M=!1,disableListWrap:O=!1,disablePortal:F=!1,filterOptions:j,filterSelectedOptions:W=!1,forcePopupIcon:q="auto",freeSolo:Z=!1,fullWidth:ee=!1,getLimitTagsText:G=Ll=>`+${Ll}`,getOptionDisabled:te,getOptionKey:Q,getOptionLabel:ie,isOptionEqualToValue:se,groupBy:de,handleHomeEndKeys:ne=!i.freeSolo,id:we,includeInputInList:ue=!1,inputValue:ce,limitTags:ye=-1,ListboxComponent:he,ListboxProps:pe,loading:me=!1,loadingText:be="Loading…",multiple:xe=!1,noOptionsText:Te="No options",onChange:Ge,onClose:tt,onHighlightChange:Ue,onInputChange:Me,onOpen:He,open:at,openOnFocus:rt=!1,openText:Be="Open",options:lt,PaperComponent:ct,PopperComponent:ze,popupIcon:Ke=NPn||(NPn=k.jsx(dui,{})),readOnly:$e=!1,renderGroup:nt,renderInput:vt,renderOption:Pt,renderTags:Ct,renderValue:Ye,selectOnFocus:wt=!i.freeSolo,size:zt="medium",slots:mn={},slotProps:xn={},value:hn,...Zi}=i,{getRootProps:$i,getInputProps:Dr,getInputLabelProps:ps,getPopupIndicatorProps:nn,getClearProps:xt,getItemProps:Ei,getListboxProps:gr,getOptionProps:ss,value:us,dirty:_r,expanded:uo,id:xs,popupOpen:Fs,focused:eo,focusedItem:Ri,anchorEl:Ls,setAnchorEl:Cr,inputValue:Sr,groupedOptions:os}=Mgr({...i,componentName:"Autocomplete"}),Ks=!I&&!A&&_r&&!$e,Ft=(!Z||q===!0)&&q!==!1,{onMouseDown:rn}=Dr(),{ref:zn,...Oi}=gr(),gs=ie||(Ll=>Ll.label??Ll),ur={...i,disablePortal:F,expanded:uo,focused:eo,fullWidth:ee,getOptionLabel:gs,hasClearIcon:Ks,hasPopupIcon:Ft,inputFocused:Ri===-1,popupOpen:Fs,size:zt},vs=f1r(ur),Ir={slots:{paper:ct,popper:ze,...mn},slotProps:{chip:d,listbox:pe,...x,...xn}},[oo,Do]=_o("listbox",{elementType:C1r,externalForwardedProps:Ir,ownerState:ur,className:vs.listbox,additionalProps:Oi,ref:zn}),[zs,cn]=_o("paper",{elementType:Jf,externalForwardedProps:Ir,ownerState:ur,className:vs.paper}),[bt,_n]=_o("popper",{elementType:c8,externalForwardedProps:Ir,ownerState:ur,className:vs.popper,additionalProps:{disablePortal:F,style:{width:Ls?Ls.clientWidth:null},role:"presentation",anchorEl:Ls,open:Fs}});let Di;const Ni=Ll=>({className:vs.tag,disabled:A,...Ei(Ll)});if(xe?us.length>0&&(Ct?Di=Ct(us,Ni,ur):Ye?Di=Ye(us,Ni,ur):Di=us.map((Ll,Pu)=>{const{key:cd,...Ag}=Ni({index:Pu});return k.jsx(N_,{label:gs(Ll),size:zt,...Ag,...Ir.slotProps.chip},cd)})):Ye&&us!=null&&(Di=Ye(us,Ni,ur)),ye>-1&&Array.isArray(Di)){const Ll=Di.length-ye;!eo&&Ll>0&&(Di=Di.splice(0,ye),Di.push(k.jsx("span",{className:vs.tag,children:G(Ll)},Di.length)))}const Es=nt||(Ll=>k.jsxs("li",{children:[k.jsx(S1r,{className:vs.groupLabel,ownerState:ur,component:"div",children:Ll.group}),k.jsx(x1r,{className:vs.groupUl,ownerState:ur,children:Ll.children})]},Ll.key)),Lu=Pt||((Ll,Pu)=>{const{key:cd,...Ag}=Ll;return k.jsx("li",{...Ag,children:gs(Pu)},cd)}),tu=(Ll,Pu)=>{const cd=ss({option:Ll,index:Pu});return Lu({...cd,className:vs.option},Ll,{selected:cd["aria-selected"],index:Pu,inputValue:Sr},ur)},Kd=Ir.slotProps.clearIndicator,ld=Ir.slotProps.popupIndicator;return k.jsxs(D.Fragment,{children:[k.jsx(p1r,{ref:t,className:_i(vs.root,h),ownerState:ur,...$i(Zi),children:vt({id:xs,disabled:A,fullWidth:i.fullWidth??!0,size:zt==="small"?"small":void 0,InputLabelProps:ps(),InputProps:{ref:Cr,className:vs.inputRoot,startAdornment:Di,onMouseDown:Ll=>{Ll.target===Ll.currentTarget&&rn(Ll)},...(Ks||Ft)&&{endAdornment:k.jsxs(g1r,{className:vs.endAdornment,ownerState:ur,children:[Ks?k.jsx(m1r,{...xt(),"aria-label":w,title:w,ownerState:ur,...Kd,className:_i(vs.clearIndicator,Kd?.className),children:p}):null,Ft?k.jsx(b1r,{...nn(),disabled:A,"aria-label":Fs?_:Be,title:Fs?_:Be,ownerState:ur,...ld,className:_i(vs.popupIndicator,ld?.className),children:Ke}):null]})}},inputProps:{className:vs.input,disabled:A,readOnly:$e,...Dr()}})}),Ls?k.jsx(v1r,{as:bt,..._n,children:k.jsxs(w1r,{as:zs,...cn,children:[me&&os.length===0?k.jsx(y1r,{className:vs.loading,ownerState:ur,children:be}):null,os.length===0&&!Z&&!me?k.jsx(_1r,{className:vs.noOptions,ownerState:ur,role:"presentation",onMouseDown:Ll=>{Ll.preventDefault()},children:Te}):null,os.length>0?k.jsx(oo,{as:he,...Do,children:os.map((Ll,Pu)=>de?Es({key:Ll.key,group:Ll.group,children:Ll.options.map((cd,Ag)=>tu(cd,Ll.index+Ag))}):tu(Ll,Pu))}):null]})}):null]})}),E1r=Ya(k.jsx("path",{d:"M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"}));function k1r(n){return No("MuiAvatar",n)}Po("MuiAvatar",["root","colorDefault","circular","rounded","square","img","fallback"]);const T1r=n=>{const{classes:e,variant:t,colorDefault:i}=n;return Fo({root:["root",t,i&&"colorDefault"],img:["img"],fallback:["fallback"]},k1r,e)},L1r=tn("div",{name:"MuiAvatar",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.variant],t.colorDefault&&e.colorDefault]}})(Gs(({theme:n})=>({position:"relative",display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0,width:40,height:40,fontFamily:n.typography.fontFamily,fontSize:n.typography.pxToRem(20),lineHeight:1,borderRadius:"50%",overflow:"hidden",userSelect:"none",variants:[{props:{variant:"rounded"},style:{borderRadius:(n.vars||n).shape.borderRadius}},{props:{variant:"square"},style:{borderRadius:0}},{props:{colorDefault:!0},style:{color:(n.vars||n).palette.background.default,...n.vars?{backgroundColor:n.vars.palette.Avatar.defaultBg}:{backgroundColor:n.palette.grey[400],...n.applyStyles("dark",{backgroundColor:n.palette.grey[600]})}}}]}))),D1r=tn("img",{name:"MuiAvatar",slot:"Img"})({width:"100%",height:"100%",textAlign:"center",objectFit:"cover",color:"transparent",textIndent:1e4}),I1r=tn(E1r,{name:"MuiAvatar",slot:"Fallback"})({width:"75%",height:"75%"});function A1r({crossOrigin:n,referrerPolicy:e,src:t,srcSet:i}){const[r,o]=D.useState(!1);return D.useEffect(()=>{if(!t&&!i)return;o(!1);let l=!0;const c=new Image;return c.onload=()=>{l&&o("loaded")},c.onerror=()=>{l&&o("error")},c.crossOrigin=n,c.referrerPolicy=e,c.src=t,i&&(c.srcset=i),()=>{l=!1}},[n,e,t,i]),r}const R1r=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiAvatar"}),{alt:r,children:o,className:l,component:c="div",slots:d={},slotProps:h={},imgProps:p,sizes:m,src:b,srcSet:w,variant:_="circular",...x}=i;let T=null;const I={...i,component:c,variant:_},L=A1r({...p,...typeof h.img=="function"?h.img(I):h.img,src:b,srcSet:w}),A=b||w,M=A&&L!=="error";I.colorDefault=!M,delete I.ownerState;const O=T1r(I),[F,j]=_o("root",{ref:t,className:_i(O.root,l),elementType:L1r,externalForwardedProps:{slots:d,slotProps:h,component:c,...x},ownerState:I}),[W,q]=_o("img",{className:O.img,elementType:D1r,externalForwardedProps:{slots:d,slotProps:{img:{...p,...h.img}}},additionalProps:{alt:r,src:b,srcSet:w,sizes:m},ownerState:I}),[Z,ee]=_o("fallback",{className:O.fallback,elementType:I1r,externalForwardedProps:{slots:d,slotProps:h},shouldForwardComponentProp:!0,ownerState:I});return M?T=k.jsx(W,{...q}):o||o===0?T=o:A&&r?T=r[0]:T=k.jsx(Z,{...ee}),k.jsx(F,{...j,children:T})}),M1r={entering:{opacity:1},entered:{opacity:1}},eY=D.forwardRef(function(e,t){const i=Lf(),r={enter:i.transitions.duration.enteringScreen,exit:i.transitions.duration.leavingScreen},{addEndListener:o,appear:l=!0,children:c,easing:d,in:h,onEnter:p,onEntered:m,onEntering:b,onExit:w,onExited:_,onExiting:x,style:T,timeout:I=r,TransitionComponent:L=l8,...A}=e,M=D.useRef(null),O=xm(M,FY(c),t),F=Q=>ie=>{if(Q){const se=M.current;ie===void 0?Q(se):Q(se,ie)}},j=F(b),W=F((Q,ie)=>{Yzt(Q);const se=QK({style:T,timeout:I,easing:d},{mode:"enter"});Q.style.webkitTransition=i.transitions.create("opacity",se),Q.style.transition=i.transitions.create("opacity",se),p&&p(Q,ie)}),q=F(m),Z=F(x),ee=F(Q=>{const ie=QK({style:T,timeout:I,easing:d},{mode:"exit"});Q.style.webkitTransition=i.transitions.create("opacity",ie),Q.style.transition=i.transitions.create("opacity",ie),w&&w(Q)}),G=F(_),te=Q=>{o&&o(M.current,Q)};return k.jsx(L,{appear:l,in:h,nodeRef:M,onEnter:W,onEntered:q,onEntering:j,onExit:ee,onExited:G,onExiting:Z,addEndListener:te,timeout:I,...A,children:(Q,{ownerState:ie,...se})=>D.cloneElement(c,{style:{opacity:0,visibility:Q==="exited"&&!h?"hidden":void 0,...M1r[Q],...T,...c.props.style},ref:O,...se})})});function O1r(n){return No("MuiBackdrop",n)}Po("MuiBackdrop",["root","invisible"]);const N1r=n=>{const{classes:e,invisible:t}=n;return Fo({root:["root",t&&"invisible"]},O1r,e)},P1r=tn("div",{name:"MuiBackdrop",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.invisible&&e.invisible]}})({position:"fixed",display:"flex",alignItems:"center",justifyContent:"center",right:0,bottom:0,top:0,left:0,backgroundColor:"rgba(0, 0, 0, 0.5)",WebkitTapHighlightColor:"transparent",variants:[{props:{invisible:!0},style:{backgroundColor:"transparent"}}]}),lUt=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiBackdrop"}),{children:r,className:o,component:l="div",invisible:c=!1,open:d,components:h={},componentsProps:p={},slotProps:m={},slots:b={},TransitionComponent:w,transitionDuration:_,...x}=i,T={...i,component:l,invisible:c},I=N1r(T),L={transition:w,root:h.Root,...b},A={...p,...m},M={component:l,slots:L,slotProps:A},[O,F]=_o("root",{elementType:P1r,externalForwardedProps:M,className:_i(I.root,o),ownerState:T}),[j,W]=_o("transition",{elementType:eY,externalForwardedProps:M,ownerState:T});return k.jsx(j,{in:d,timeout:_,...x,...W,children:k.jsx(O,{"aria-hidden":!0,...F,ref:t,children:r})})});function F1r(n){const{badgeContent:e,invisible:t=!1,max:i=99,showZero:r=!1}=n,o=Jzt({badgeContent:e,max:i});let l=t;t===!1&&e===0&&!r&&(l=!0);const{badgeContent:c,max:d=i}=l?o:n,h=c&&Number(c)>d?`${d}+`:c;return{badgeContent:c,invisible:l,max:d,displayValue:h}}function j1r(n){return No("MuiBadge",n)}const H1r=Po("MuiBadge",["root","badge","dot","standard","anchorOriginTopRight","anchorOriginBottomRight","anchorOriginTopLeft","anchorOriginBottomLeft","invisible","colorError","colorInfo","colorPrimary","colorSecondary","colorSuccess","colorWarning","overlapRectangular","overlapCircular","anchorOriginTopLeftCircular","anchorOriginTopLeftRectangular","anchorOriginTopRightCircular","anchorOriginTopRightRectangular","anchorOriginBottomLeftCircular","anchorOriginBottomLeftRectangular","anchorOriginBottomRightCircular","anchorOriginBottomRightRectangular"]),PEt=10,FEt=4,B1r=n=>{const{color:e,anchorOrigin:t,invisible:i,overlap:r,variant:o,classes:l={}}=n,c={root:["root"],badge:["badge",o,i&&"invisible",`anchorOrigin${ii(t.vertical)}${ii(t.horizontal)}`,`anchorOrigin${ii(t.vertical)}${ii(t.horizontal)}${ii(r)}`,`overlap${ii(r)}`,e!=="default"&&`color${ii(e)}`]};return Fo(c,j1r,l)},W1r=tn("span",{name:"MuiBadge",slot:"Root"})({position:"relative",display:"inline-flex",verticalAlign:"middle",flexShrink:0}),V1r=tn("span",{name:"MuiBadge",slot:"Badge",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.badge,e[t.variant],e[`anchorOrigin${ii(t.anchorOrigin.vertical)}${ii(t.anchorOrigin.horizontal)}${ii(t.overlap)}`],t.color!=="default"&&e[`color${ii(t.color)}`],t.invisible&&e.invisible]}})(Gs(({theme:n})=>({display:"flex",flexDirection:"row",flexWrap:"wrap",justifyContent:"center",alignContent:"center",alignItems:"center",position:"absolute",boxSizing:"border-box",fontFamily:n.typography.fontFamily,fontWeight:n.typography.fontWeightMedium,fontSize:n.typography.pxToRem(12),minWidth:PEt*2,lineHeight:1,padding:"0 6px",height:PEt*2,borderRadius:PEt,zIndex:1,transition:n.transitions.create("transform",{easing:n.transitions.easing.easeInOut,duration:n.transitions.duration.enteringScreen}),variants:[...Object.entries(n.palette).filter(Vh(["contrastText"])).map(([e])=>({props:{color:e},style:{backgroundColor:(n.vars||n).palette[e].main,color:(n.vars||n).palette[e].contrastText}})),{props:{variant:"dot"},style:{borderRadius:FEt,height:FEt*2,minWidth:FEt*2,padding:0}},{props:{invisible:!0},style:{transition:n.transitions.create("transform",{easing:n.transitions.easing.easeInOut,duration:n.transitions.duration.leavingScreen})}},{style:({ownerState:e})=>{const{vertical:t,horizontal:i}=e.anchorOrigin,r=e.overlap==="circular"?"14%":0;return{"--Badge-translateX":i==="right"?"50%":"-50%","--Badge-translateY":t==="top"?"-50%":"50%",top:t==="top"?r:"initial",bottom:t==="bottom"?r:"initial",right:i==="right"?r:"initial",left:i==="left"?r:"initial",transform:"scale(1) translate(var(--Badge-translateX), var(--Badge-translateY))",transformOrigin:`${i==="right"?"100%":"0%"} ${t==="top"?"0%":"100%"}`,[`&.${H1r.invisible}`]:{transform:"scale(0) translate(var(--Badge-translateX), var(--Badge-translateY))"}}}}]})));function PPn(n){return{vertical:n?.vertical??"top",horizontal:n?.horizontal??"right"}}const $1r=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiBadge"}),{anchorOrigin:r,className:o,classes:l,component:c,components:d={},componentsProps:h={},children:p,overlap:m="rectangular",color:b="default",invisible:w=!1,max:_=99,badgeContent:x,slots:T,slotProps:I,showZero:L=!1,variant:A="standard",...M}=i,{badgeContent:O,invisible:F,max:j,displayValue:W}=F1r({max:_,invisible:w,badgeContent:x,showZero:L}),q=Jzt({anchorOrigin:PPn(r),color:b,overlap:m,variant:A,badgeContent:x}),Z=F||O==null&&A!=="dot",{color:ee=b,overlap:G=m,anchorOrigin:te,variant:Q=A}=Z?q:i,ie=PPn(te),se=Q!=="dot"?W:void 0,de={...i,badgeContent:O,invisible:Z,max:j,displayValue:se,showZero:L,anchorOrigin:ie,color:ee,overlap:G,variant:Q},ne=B1r(de),we={slots:{root:T?.root??d.Root,badge:T?.badge??d.Badge},slotProps:{root:I?.root??h.root,badge:I?.badge??h.badge}},[ue,ce]=_o("root",{elementType:W1r,externalForwardedProps:{...we,...M},ownerState:de,className:_i(ne.root,o),ref:t,additionalProps:{as:c}}),[ye,he]=_o("badge",{elementType:V1r,externalForwardedProps:we,ownerState:de,className:ne.badge});return k.jsxs(ue,{...ce,children:[p,k.jsx(ye,{...he,children:se})]})}),z1r=Po("MuiBox",["root"]),U1r=G1e(),Re=aci({themeId:w3,defaultTheme:U1r,defaultClassName:z1r.root,generateClassName:Izt.generate}),q1r=Ya(k.jsx("path",{d:"M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"})),G1r=tn(D3,{name:"MuiBreadcrumbCollapsed"})(Gs(({theme:n})=>({display:"flex",marginLeft:`calc(${n.spacing(1)} * 0.5)`,marginRight:`calc(${n.spacing(1)} * 0.5)`,...n.palette.mode==="light"?{backgroundColor:n.palette.grey[100],color:n.palette.grey[700]}:{backgroundColor:n.palette.grey[700],color:n.palette.grey[100]},borderRadius:2,"&:hover, &:focus":{...n.palette.mode==="light"?{backgroundColor:n.palette.grey[200]}:{backgroundColor:n.palette.grey[600]}},"&:active":{boxShadow:n.shadows[0],...n.palette.mode==="light"?{backgroundColor:lLe(n.palette.grey[200],.12)}:{backgroundColor:lLe(n.palette.grey[600],.12)}}}))),K1r=tn(q1r)({width:24,height:16});function Y1r(n){const{slots:e={},slotProps:t={},...i}=n,r=n;return k.jsx("li",{children:k.jsx(G1r,{focusRipple:!0,...i,ownerState:r,children:k.jsx(K1r,{as:e.CollapsedIcon,ownerState:r,...t.collapsedIcon})})})}function Z1r(n){return No("MuiBreadcrumbs",n)}const X1r=Po("MuiBreadcrumbs",["root","ol","li","separator"]),Q1r=n=>{const{classes:e}=n;return Fo({root:["root"],li:["li"],ol:["ol"],separator:["separator"]},Z1r,e)},J1r=tn(di,{name:"MuiBreadcrumbs",slot:"Root",overridesResolver:(n,e)=>[{[`& .${X1r.li}`]:e.li},e.root]})({}),e0r=tn("ol",{name:"MuiBreadcrumbs",slot:"Ol"})({display:"flex",flexWrap:"wrap",alignItems:"center",padding:0,margin:0,listStyle:"none"}),t0r=tn("li",{name:"MuiBreadcrumbs",slot:"Separator"})({display:"flex",userSelect:"none",marginLeft:8,marginRight:8});function n0r(n,e,t,i){return n.reduce((r,o,l)=>(l{const W=()=>{T(!0);const q=M.current.querySelector("a[href],button,[tabindex]");q&&q.focus()};return m+p>=j.length?j:[...j.slice(0,m),k.jsx(Y1r,{"aria-label":h,slots:{CollapsedIcon:c.CollapsedIcon},slotProps:{collapsedIcon:A},onClick:W},"ellipsis"),...j.slice(j.length-p,j.length)]},F=D.Children.toArray(r).filter(j=>D.isValidElement(j)).map((j,W)=>k.jsx("li",{className:L.li,children:j},`child-${W}`));return k.jsx(J1r,{ref:t,component:l,color:"textSecondary",className:_i(L.root,o),ownerState:I,..._,children:k.jsx(e0r,{className:L.ol,ref:M,ownerState:I,children:n0r(x||b&&F.length<=b?F:O(F),L.separator,w,I)})})});function r0r(n){return No("MuiButton",n)}const fte=Po("MuiButton",["root","text","textInherit","textPrimary","textSecondary","textSuccess","textError","textInfo","textWarning","outlined","outlinedInherit","outlinedPrimary","outlinedSecondary","outlinedSuccess","outlinedError","outlinedInfo","outlinedWarning","contained","containedInherit","containedPrimary","containedSecondary","containedSuccess","containedError","containedInfo","containedWarning","disableElevation","focusVisible","disabled","colorInherit","colorPrimary","colorSecondary","colorSuccess","colorError","colorInfo","colorWarning","textSizeSmall","textSizeMedium","textSizeLarge","outlinedSizeSmall","outlinedSizeMedium","outlinedSizeLarge","containedSizeSmall","containedSizeMedium","containedSizeLarge","sizeMedium","sizeSmall","sizeLarge","fullWidth","startIcon","endIcon","icon","iconSizeSmall","iconSizeMedium","iconSizeLarge","loading","loadingWrapper","loadingIconPlaceholder","loadingIndicator","loadingPositionCenter","loadingPositionStart","loadingPositionEnd"]),hui=D.createContext({}),fui=D.createContext(void 0),s0r=n=>{const{color:e,disableElevation:t,fullWidth:i,size:r,variant:o,loading:l,loadingPosition:c,classes:d}=n,h={root:["root",l&&"loading",o,`${o}${ii(e)}`,`size${ii(r)}`,`${o}Size${ii(r)}`,`color${ii(e)}`,t&&"disableElevation",i&&"fullWidth",l&&`loadingPosition${ii(c)}`],startIcon:["icon","startIcon",`iconSize${ii(r)}`],endIcon:["icon","endIcon",`iconSize${ii(r)}`],loadingIndicator:["loadingIndicator"],loadingWrapper:["loadingWrapper"]},p=Fo(h,r0r,d);return{...d,...p}},pui=[{props:{size:"small"},style:{"& > *:nth-of-type(1)":{fontSize:18}}},{props:{size:"medium"},style:{"& > *:nth-of-type(1)":{fontSize:20}}},{props:{size:"large"},style:{"& > *:nth-of-type(1)":{fontSize:22}}}],o0r=tn(D3,{shouldForwardProp:n=>B_(n)||n==="classes",name:"MuiButton",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.variant],e[`${t.variant}${ii(t.color)}`],e[`size${ii(t.size)}`],e[`${t.variant}Size${ii(t.size)}`],t.color==="inherit"&&e.colorInherit,t.disableElevation&&e.disableElevation,t.fullWidth&&e.fullWidth,t.loading&&e.loading]}})(Gs(({theme:n})=>{const e=n.palette.mode==="light"?n.palette.grey[300]:n.palette.grey[800],t=n.palette.mode==="light"?n.palette.grey.A100:n.palette.grey[700];return{...n.typography.button,minWidth:64,padding:"6px 16px",border:0,borderRadius:(n.vars||n).shape.borderRadius,transition:n.transitions.create(["background-color","box-shadow","border-color","color"],{duration:n.transitions.duration.short}),"&:hover":{textDecoration:"none"},[`&.${fte.disabled}`]:{color:(n.vars||n).palette.action.disabled},variants:[{props:{variant:"contained"},style:{color:"var(--variant-containedColor)",backgroundColor:"var(--variant-containedBg)",boxShadow:(n.vars||n).shadows[2],"&:hover":{boxShadow:(n.vars||n).shadows[4],"@media (hover: none)":{boxShadow:(n.vars||n).shadows[2]}},"&:active":{boxShadow:(n.vars||n).shadows[8]},[`&.${fte.focusVisible}`]:{boxShadow:(n.vars||n).shadows[6]},[`&.${fte.disabled}`]:{color:(n.vars||n).palette.action.disabled,boxShadow:(n.vars||n).shadows[0],backgroundColor:(n.vars||n).palette.action.disabledBackground}}},{props:{variant:"outlined"},style:{padding:"5px 15px",border:"1px solid currentColor",borderColor:"var(--variant-outlinedBorder, currentColor)",backgroundColor:"var(--variant-outlinedBg)",color:"var(--variant-outlinedColor)",[`&.${fte.disabled}`]:{border:`1px solid ${(n.vars||n).palette.action.disabledBackground}`}}},{props:{variant:"text"},style:{padding:"6px 8px",color:"var(--variant-textColor)",backgroundColor:"var(--variant-textBg)"}},...Object.entries(n.palette).filter(Vh()).map(([i])=>({props:{color:i},style:{"--variant-textColor":(n.vars||n).palette[i].main,"--variant-outlinedColor":(n.vars||n).palette[i].main,"--variant-outlinedBorder":n.alpha((n.vars||n).palette[i].main,.5),"--variant-containedColor":(n.vars||n).palette[i].contrastText,"--variant-containedBg":(n.vars||n).palette[i].main,"@media (hover: hover)":{"&:hover":{"--variant-containedBg":(n.vars||n).palette[i].dark,"--variant-textBg":n.alpha((n.vars||n).palette[i].main,(n.vars||n).palette.action.hoverOpacity),"--variant-outlinedBorder":(n.vars||n).palette[i].main,"--variant-outlinedBg":n.alpha((n.vars||n).palette[i].main,(n.vars||n).palette.action.hoverOpacity)}}}})),{props:{color:"inherit"},style:{color:"inherit",borderColor:"currentColor","--variant-containedBg":n.vars?n.vars.palette.Button.inheritContainedBg:e,"@media (hover: hover)":{"&:hover":{"--variant-containedBg":n.vars?n.vars.palette.Button.inheritContainedHoverBg:t,"--variant-textBg":n.alpha((n.vars||n).palette.text.primary,(n.vars||n).palette.action.hoverOpacity),"--variant-outlinedBg":n.alpha((n.vars||n).palette.text.primary,(n.vars||n).palette.action.hoverOpacity)}}}},{props:{size:"small",variant:"text"},style:{padding:"4px 5px",fontSize:n.typography.pxToRem(13)}},{props:{size:"large",variant:"text"},style:{padding:"8px 11px",fontSize:n.typography.pxToRem(15)}},{props:{size:"small",variant:"outlined"},style:{padding:"3px 9px",fontSize:n.typography.pxToRem(13)}},{props:{size:"large",variant:"outlined"},style:{padding:"7px 21px",fontSize:n.typography.pxToRem(15)}},{props:{size:"small",variant:"contained"},style:{padding:"4px 10px",fontSize:n.typography.pxToRem(13)}},{props:{size:"large",variant:"contained"},style:{padding:"8px 22px",fontSize:n.typography.pxToRem(15)}},{props:{disableElevation:!0},style:{boxShadow:"none","&:hover":{boxShadow:"none"},[`&.${fte.focusVisible}`]:{boxShadow:"none"},"&:active":{boxShadow:"none"},[`&.${fte.disabled}`]:{boxShadow:"none"}}},{props:{fullWidth:!0},style:{width:"100%"}},{props:{loadingPosition:"center"},style:{transition:n.transitions.create(["background-color","box-shadow","border-color"],{duration:n.transitions.duration.short}),[`&.${fte.loading}`]:{color:"transparent"}}}]}})),a0r=tn("span",{name:"MuiButton",slot:"StartIcon",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.startIcon,t.loading&&e.startIconLoadingStart,e[`iconSize${ii(t.size)}`]]}})(({theme:n})=>({display:"inherit",marginRight:8,marginLeft:-4,variants:[{props:{size:"small"},style:{marginLeft:-2}},{props:{loadingPosition:"start",loading:!0},style:{transition:n.transitions.create(["opacity"],{duration:n.transitions.duration.short}),opacity:0}},{props:{loadingPosition:"start",loading:!0,fullWidth:!0},style:{marginRight:-8}},...pui]})),l0r=tn("span",{name:"MuiButton",slot:"EndIcon",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.endIcon,t.loading&&e.endIconLoadingEnd,e[`iconSize${ii(t.size)}`]]}})(({theme:n})=>({display:"inherit",marginRight:-4,marginLeft:8,variants:[{props:{size:"small"},style:{marginRight:-2}},{props:{loadingPosition:"end",loading:!0},style:{transition:n.transitions.create(["opacity"],{duration:n.transitions.duration.short}),opacity:0}},{props:{loadingPosition:"end",loading:!0,fullWidth:!0},style:{marginLeft:-8}},...pui]})),c0r=tn("span",{name:"MuiButton",slot:"LoadingIndicator"})(({theme:n})=>({display:"none",position:"absolute",visibility:"visible",variants:[{props:{loading:!0},style:{display:"flex"}},{props:{loadingPosition:"start"},style:{left:14}},{props:{loadingPosition:"start",size:"small"},style:{left:10}},{props:{variant:"text",loadingPosition:"start"},style:{left:6}},{props:{loadingPosition:"center"},style:{left:"50%",transform:"translate(-50%)",color:(n.vars||n).palette.action.disabled}},{props:{loadingPosition:"end"},style:{right:14}},{props:{loadingPosition:"end",size:"small"},style:{right:10}},{props:{variant:"text",loadingPosition:"end"},style:{right:6}},{props:{loadingPosition:"start",fullWidth:!0},style:{position:"relative",left:-10}},{props:{loadingPosition:"end",fullWidth:!0},style:{position:"relative",right:-10}}]})),FPn=tn("span",{name:"MuiButton",slot:"LoadingIconPlaceholder"})({display:"inline-block",width:"1em",height:"1em"}),cs=D.forwardRef(function(e,t){const i=D.useContext(hui),r=D.useContext(fui),o=aLe(i,e),l=Wo({props:o,name:"MuiButton"}),{children:c,color:d="primary",component:h="button",className:p,disabled:m=!1,disableElevation:b=!1,disableFocusRipple:w=!1,endIcon:_,focusVisibleClassName:x,fullWidth:T=!1,id:I,loading:L=null,loadingIndicator:A,loadingPosition:M="center",size:O="medium",startIcon:F,type:j,variant:W="text",...q}=l,Z=YW(I),ee=A??k.jsx(fv,{"aria-labelledby":Z,color:"inherit",size:16}),G={...l,color:d,component:h,disabled:m,disableElevation:b,disableFocusRipple:w,fullWidth:T,loading:L,loadingIndicator:ee,loadingPosition:M,size:O,type:j,variant:W},te=s0r(G),Q=(F||L&&M==="start")&&k.jsx(a0r,{className:te.startIcon,ownerState:G,children:F||k.jsx(FPn,{className:te.loadingIconPlaceholder,ownerState:G})}),ie=(_||L&&M==="end")&&k.jsx(l0r,{className:te.endIcon,ownerState:G,children:_||k.jsx(FPn,{className:te.loadingIconPlaceholder,ownerState:G})}),se=r||"",de=typeof L=="boolean"?k.jsx("span",{className:te.loadingWrapper,style:{display:"contents"},children:L&&k.jsx(c0r,{className:te.loadingIndicator,ownerState:G,children:ee})}):null;return k.jsxs(o0r,{ownerState:G,className:_i(i.className,te.root,p,se),component:h,disabled:m||L,focusRipple:!w,focusVisibleClassName:_i(te.focusVisible,x),ref:t,type:j,id:L?Z:I,...q,classes:te,children:[Q,M!=="end"&&de,c,M==="end"&&de,ie]})});function u0r(n){return D.Children.toArray(n).filter(e=>D.isValidElement(e))}function d0r(n){return No("MuiButtonGroup",n)}const Cu=Po("MuiButtonGroup",["root","contained","outlined","text","disableElevation","disabled","firstButton","fullWidth","horizontal","vertical","colorPrimary","colorSecondary","grouped","groupedHorizontal","groupedVertical","groupedText","groupedTextHorizontal","groupedTextVertical","groupedTextPrimary","groupedTextSecondary","groupedOutlined","groupedOutlinedHorizontal","groupedOutlinedVertical","groupedOutlinedPrimary","groupedOutlinedSecondary","groupedContained","groupedContainedHorizontal","groupedContainedVertical","groupedContainedPrimary","groupedContainedSecondary","lastButton","middleButton"]),h0r=(n,e)=>{const{ownerState:t}=n;return[{[`& .${Cu.grouped}`]:e.grouped},{[`& .${Cu.grouped}`]:e[`grouped${ii(t.orientation)}`]},{[`& .${Cu.grouped}`]:e[`grouped${ii(t.variant)}`]},{[`& .${Cu.grouped}`]:e[`grouped${ii(t.variant)}${ii(t.orientation)}`]},{[`& .${Cu.grouped}`]:e[`grouped${ii(t.variant)}${ii(t.color)}`]},{[`& .${Cu.firstButton}`]:e.firstButton},{[`& .${Cu.lastButton}`]:e.lastButton},{[`& .${Cu.middleButton}`]:e.middleButton},e.root,e[t.variant],t.disableElevation===!0&&e.disableElevation,t.fullWidth&&e.fullWidth,t.orientation==="vertical"&&e.vertical]},f0r=n=>{const{classes:e,color:t,disabled:i,disableElevation:r,fullWidth:o,orientation:l,variant:c}=n,d={root:["root",c,l,o&&"fullWidth",r&&"disableElevation",`color${ii(t)}`],grouped:["grouped",`grouped${ii(l)}`,`grouped${ii(c)}`,`grouped${ii(c)}${ii(l)}`,`grouped${ii(c)}${ii(t)}`,i&&"disabled"],firstButton:["firstButton"],lastButton:["lastButton"],middleButton:["middleButton"]};return Fo(d,d0r,e)},p0r=tn("div",{name:"MuiButtonGroup",slot:"Root",overridesResolver:h0r})(Gs(({theme:n})=>({display:"inline-flex",borderRadius:(n.vars||n).shape.borderRadius,variants:[{props:{variant:"contained"},style:{boxShadow:(n.vars||n).shadows[2],[`& .${Cu.grouped}`]:{boxShadow:"none","&:hover":{boxShadow:"none"}}}},{props:{disableElevation:!0},style:{boxShadow:"none"}},{props:{fullWidth:!0},style:{width:"100%"}},{props:{orientation:"vertical"},style:{flexDirection:"column",[`& .${Cu.lastButton},& .${Cu.middleButton}`]:{borderTopRightRadius:0,borderTopLeftRadius:0},[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderBottomRightRadius:0,borderBottomLeftRadius:0}}},{props:{orientation:"horizontal"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderTopRightRadius:0,borderBottomRightRadius:0},[`& .${Cu.lastButton},& .${Cu.middleButton}`]:{borderTopLeftRadius:0,borderBottomLeftRadius:0}}},{props:{variant:"text",orientation:"horizontal"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderRight:n.vars?`1px solid ${n.alpha(n.vars.palette.common.onBackground,.23)}`:`1px solid ${n.palette.mode==="light"?"rgba(0, 0, 0, 0.23)":"rgba(255, 255, 255, 0.23)"}`,[`&.${Cu.disabled}`]:{borderRight:`1px solid ${(n.vars||n).palette.action.disabled}`}}}},{props:{variant:"text",orientation:"vertical"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderBottom:n.vars?`1px solid ${n.alpha(n.vars.palette.common.onBackground,.23)}`:`1px solid ${n.palette.mode==="light"?"rgba(0, 0, 0, 0.23)":"rgba(255, 255, 255, 0.23)"}`,[`&.${Cu.disabled}`]:{borderBottom:`1px solid ${(n.vars||n).palette.action.disabled}`}}}},...Object.entries(n.palette).filter(Vh()).flatMap(([e])=>[{props:{variant:"text",color:e},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderColor:n.alpha((n.vars||n).palette[e].main,.5)}}}]),{props:{variant:"outlined",orientation:"horizontal"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderRightColor:"transparent","&:hover":{borderRightColor:"currentColor"}},[`& .${Cu.lastButton},& .${Cu.middleButton}`]:{marginLeft:-1}}},{props:{variant:"outlined",orientation:"vertical"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderBottomColor:"transparent","&:hover":{borderBottomColor:"currentColor"}},[`& .${Cu.lastButton},& .${Cu.middleButton}`]:{marginTop:-1}}},{props:{variant:"contained",orientation:"horizontal"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderRight:`1px solid ${(n.vars||n).palette.grey[400]}`,[`&.${Cu.disabled}`]:{borderRight:`1px solid ${(n.vars||n).palette.action.disabled}`}}}},{props:{variant:"contained",orientation:"vertical"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderBottom:`1px solid ${(n.vars||n).palette.grey[400]}`,[`&.${Cu.disabled}`]:{borderBottom:`1px solid ${(n.vars||n).palette.action.disabled}`}}}},...Object.entries(n.palette).filter(Vh(["dark"])).map(([e])=>({props:{variant:"contained",color:e},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderColor:(n.vars||n).palette[e].dark}}}))],[`& .${Cu.grouped}`]:{minWidth:40}}))),gui=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiButtonGroup"}),{children:r,className:o,color:l="primary",component:c="div",disabled:d=!1,disableElevation:h=!1,disableFocusRipple:p=!1,disableRipple:m=!1,fullWidth:b=!1,orientation:w="horizontal",size:_="medium",variant:x="outlined",...T}=i,I={...i,color:l,component:c,disabled:d,disableElevation:h,disableFocusRipple:p,disableRipple:m,fullWidth:b,orientation:w,size:_,variant:x},L=f0r(I),A=D.useMemo(()=>({className:L.grouped,color:l,disabled:d,disableElevation:h,disableFocusRipple:p,disableRipple:m,fullWidth:b,size:_,variant:x}),[l,d,h,p,m,b,_,x,L.grouped]),M=u0r(r),O=M.length,F=j=>{const W=j===0,q=j===O-1;return W&&q?"":W?L.firstButton:q?L.lastButton:L.middleButton};return k.jsx(p0r,{as:c,role:"group",className:_i(L.root,o),ref:t,ownerState:I,...T,children:k.jsx(hui.Provider,{value:A,children:M.map((j,W)=>k.jsx(fui.Provider,{value:F(W),children:j},W))})})});function g0r(n){return No("PrivateSwitchBase",n)}Po("PrivateSwitchBase",["root","checked","disabled","input","edgeStart","edgeEnd"]);const m0r=n=>{const{classes:e,checked:t,disabled:i,edge:r}=n,o={root:["root",t&&"checked",i&&"disabled",r&&`edge${ii(r)}`],input:["input"]};return Fo(o,g0r,e)},b0r=tn(D3,{name:"MuiSwitchBase"})({padding:9,borderRadius:"50%",variants:[{props:{edge:"start",size:"small"},style:{marginLeft:-3}},{props:({edge:n,ownerState:e})=>n==="start"&&e.size!=="small",style:{marginLeft:-12}},{props:{edge:"end",size:"small"},style:{marginRight:-3}},{props:({edge:n,ownerState:e})=>n==="end"&&e.size!=="small",style:{marginRight:-12}}]}),v0r=tn("input",{name:"MuiSwitchBase",shouldForwardProp:B_})({cursor:"inherit",position:"absolute",opacity:0,width:"100%",height:"100%",top:0,left:0,margin:0,padding:0,zIndex:1}),cUt=D.forwardRef(function(e,t){const{autoFocus:i,checked:r,checkedIcon:o,defaultChecked:l,disabled:c,disableFocusRipple:d=!1,edge:h=!1,icon:p,id:m,inputProps:b,inputRef:w,name:_,onBlur:x,onChange:T,onFocus:I,readOnly:L,required:A=!1,tabIndex:M,type:O,value:F,slots:j={},slotProps:W={},...q}=e,[Z,ee]=S9({controlled:r,default:!!l,name:"SwitchBase",state:"checked"}),G=DM(),te=me=>{I&&I(me),G&&G.onFocus&&G.onFocus(me)},Q=me=>{x&&x(me),G&&G.onBlur&&G.onBlur(me)},ie=me=>{if(me.nativeEvent.defaultPrevented||L)return;const be=me.target.checked;ee(be),T&&T(me,be)};let se=c;G&&typeof se>"u"&&(se=G.disabled);const de=O==="checkbox"||O==="radio",ne={...e,checked:Z,disabled:se,disableFocusRipple:d,edge:h},we=m0r(ne),ue={slots:j,slotProps:{input:b,...W}},[ce,ye]=_o("root",{ref:t,elementType:b0r,className:we.root,shouldForwardComponentProp:!0,externalForwardedProps:{...ue,component:"span",...q},getSlotProps:me=>({...me,onFocus:be=>{me.onFocus?.(be),te(be)},onBlur:be=>{me.onBlur?.(be),Q(be)}}),ownerState:ne,additionalProps:{centerRipple:!0,focusRipple:!d,role:void 0,tabIndex:null}}),[he,pe]=_o("input",{ref:w,elementType:v0r,className:we.input,externalForwardedProps:ue,getSlotProps:me=>({...me,onChange:be=>{me.onChange?.(be),ie(be)}}),ownerState:ne,additionalProps:{autoFocus:i,checked:r,defaultChecked:l,disabled:se,id:de?m:void 0,name:_,readOnly:L,required:A,tabIndex:M,type:O,...O==="checkbox"&&F===void 0?{}:{value:F}}});return k.jsxs(ce,{...ye,children:[k.jsx(he,{...pe}),Z?o:p]})}),w0r=Ya(k.jsx("path",{d:"M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"})),y0r=Ya(k.jsx("path",{d:"M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"})),_0r=Ya(k.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10H7v-2h10v2z"}));function C0r(n){return No("MuiCheckbox",n)}const jEt=Po("MuiCheckbox",["root","checked","disabled","indeterminate","colorPrimary","colorSecondary","sizeSmall","sizeMedium"]),S0r=n=>{const{classes:e,indeterminate:t,color:i,size:r}=n,o={root:["root",t&&"indeterminate",`color${ii(i)}`,`size${ii(r)}`]},l=Fo(o,C0r,e);return{...e,...l}},x0r=tn(cUt,{shouldForwardProp:n=>B_(n)||n==="classes",name:"MuiCheckbox",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.indeterminate&&e.indeterminate,e[`size${ii(t.size)}`],t.color!=="default"&&e[`color${ii(t.color)}`]]}})(Gs(({theme:n})=>({color:(n.vars||n).palette.text.secondary,variants:[{props:{color:"default",disableRipple:!1},style:{"&:hover":{backgroundColor:n.alpha((n.vars||n).palette.action.active,(n.vars||n).palette.action.hoverOpacity)}}},...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e,disableRipple:!1},style:{"&:hover":{backgroundColor:n.alpha((n.vars||n).palette[e].main,(n.vars||n).palette.action.hoverOpacity)}}})),...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{[`&.${jEt.checked}, &.${jEt.indeterminate}`]:{color:(n.vars||n).palette[e].main},[`&.${jEt.disabled}`]:{color:(n.vars||n).palette.action.disabled}}})),{props:{disableRipple:!1},style:{"&:hover":{"@media (hover: none)":{backgroundColor:"transparent"}}}}]}))),E0r=k.jsx(y0r,{}),k0r=k.jsx(w0r,{}),T0r=k.jsx(_0r,{}),Hfe=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiCheckbox"}),{checkedIcon:r=E0r,color:o="primary",icon:l=k0r,indeterminate:c=!1,indeterminateIcon:d=T0r,inputProps:h,size:p="medium",disableRipple:m=!1,className:b,slots:w={},slotProps:_={},...x}=i,T=c?d:l,I=c?d:r,L={...i,disableRipple:m,color:o,indeterminate:c,size:p},A=S0r(L),M=_.input??h,[O,F]=_o("root",{ref:t,elementType:x0r,className:_i(A.root,b),shouldForwardComponentProp:!0,externalForwardedProps:{slots:w,slotProps:_,...x},ownerState:L,additionalProps:{type:"checkbox",icon:D.cloneElement(T,{fontSize:T.props.fontSize??p}),checkedIcon:D.cloneElement(I,{fontSize:I.props.fontSize??p}),disableRipple:m,slots:w,slotProps:{input:Uzt(typeof M=="function"?M(L):M,{"data-indeterminate":c})}}});return k.jsx(O,{...F,classes:A})});function jPn(n){return n.substring(2).toLowerCase()}function L0r(n,e){return e.documentElement.clientWidth(setTimeout(()=>{d.current=!0},0),()=>{d.current=!1}),[]);const p=xm(FY(e),c),m=ub(_=>{const x=h.current;h.current=!1;const T=hv(c.current);if(!d.current||!c.current||"clientX"in _&&L0r(_,T))return;if(l.current){l.current=!1;return}let I;_.composedPath?I=_.composedPath().includes(c.current):I=!T.documentElement.contains(_.target)||c.current.contains(_.target),!I&&(t||!x)&&r(_)}),b=_=>x=>{h.current=!0;const T=e.props[_];T&&T(x)},w={ref:p};return o!==!1&&(w[o]=b(o)),D.useEffect(()=>{if(o!==!1){const _=jPn(o),x=hv(c.current),T=()=>{l.current=!0};return x.addEventListener(_,m),x.addEventListener("touchmove",T),()=>{x.removeEventListener(_,m),x.removeEventListener("touchmove",T)}}},[m,o]),i!==!1&&(w[i]=b(i)),D.useEffect(()=>{if(i!==!1){const _=jPn(i),x=hv(c.current);return x.addEventListener(_,m),()=>{x.removeEventListener(_,m)}}},[m,i]),D.cloneElement(e,w)}const D0r={track:"#2b2b2b",thumb:"#6b6b6b",active:"#959595"};function I0r(n=D0r){return{scrollbarColor:`${n.thumb} ${n.track}`,"&::-webkit-scrollbar, & *::-webkit-scrollbar":{backgroundColor:n.track},"&::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb":{borderRadius:8,backgroundColor:n.thumb,minHeight:24,border:`3px solid ${n.track}`},"&::-webkit-scrollbar-thumb:focus, & *::-webkit-scrollbar-thumb:focus":{backgroundColor:n.active},"&::-webkit-scrollbar-thumb:active, & *::-webkit-scrollbar-thumb:active":{backgroundColor:n.active},"&::-webkit-scrollbar-thumb:hover, & *::-webkit-scrollbar-thumb:hover":{backgroundColor:n.active},"&::-webkit-scrollbar-corner, & *::-webkit-scrollbar-corner":{backgroundColor:n.track}}}function mui(n=window){const e=n.document.documentElement.clientWidth;return n.innerWidth-e}function A0r(n){const e=hv(n);return e.body===n?Y6(n).innerWidth>e.documentElement.clientWidth:n.scrollHeight>n.clientHeight}function a4e(n,e){e?n.setAttribute("aria-hidden","true"):n.removeAttribute("aria-hidden")}function HPn(n){return parseFloat(Y6(n).getComputedStyle(n).paddingRight)||0}function R0r(n){const t=["TEMPLATE","SCRIPT","STYLE","LINK","MAP","META","NOSCRIPT","PICTURE","COL","COLGROUP","PARAM","SLOT","SOURCE","TRACK"].includes(n.tagName),i=n.tagName==="INPUT"&&n.getAttribute("type")==="hidden";return t||i}function BPn(n,e,t,i,r){const o=[e,t,...i];[].forEach.call(n.children,l=>{const c=!o.includes(l),d=!R0r(l);c&&d&&a4e(l,r)})}function HEt(n,e){let t=-1;return n.some((i,r)=>e(i)?(t=r,!0):!1),t}function M0r(n,e){const t=[],i=n.container;if(!e.disableScrollLock){if(A0r(i)){const l=mui(Y6(i));t.push({value:i.style.paddingRight,property:"padding-right",el:i}),i.style.paddingRight=`${HPn(i)+l}px`;const c=hv(i).querySelectorAll(".mui-fixed");[].forEach.call(c,d=>{t.push({value:d.style.paddingRight,property:"padding-right",el:d}),d.style.paddingRight=`${HPn(d)+l}px`})}let o;if(i.parentNode instanceof DocumentFragment)o=hv(i).body;else{const l=i.parentElement,c=Y6(i);o=l?.nodeName==="HTML"&&c.getComputedStyle(l).overflowY==="scroll"?l:i}t.push({value:o.style.overflow,property:"overflow",el:o},{value:o.style.overflowX,property:"overflow-x",el:o},{value:o.style.overflowY,property:"overflow-y",el:o}),o.style.overflow="hidden"}return()=>{t.forEach(({value:o,el:l,property:c})=>{o?l.style.setProperty(c,o):l.style.removeProperty(c)})}}function O0r(n){const e=[];return[].forEach.call(n.children,t=>{t.getAttribute("aria-hidden")==="true"&&e.push(t)}),e}class N0r{constructor(){this.modals=[],this.containers=[]}add(e,t){let i=this.modals.indexOf(e);if(i!==-1)return i;i=this.modals.length,this.modals.push(e),e.modalRef&&a4e(e.modalRef,!1);const r=O0r(t);BPn(t,e.mount,e.modalRef,r,!0);const o=HEt(this.containers,l=>l.container===t);return o!==-1?(this.containers[o].modals.push(e),i):(this.containers.push({modals:[e],container:t,restore:null,hiddenSiblings:r}),i)}mount(e,t){const i=HEt(this.containers,o=>o.modals.includes(e)),r=this.containers[i];r.restore||(r.restore=M0r(r,t))}remove(e,t=!0){const i=this.modals.indexOf(e);if(i===-1)return i;const r=HEt(this.containers,l=>l.modals.includes(e)),o=this.containers[r];if(o.modals.splice(o.modals.indexOf(e),1),this.modals.splice(i,1),o.modals.length===0)o.restore&&o.restore(),e.modalRef&&a4e(e.modalRef,t),BPn(o.container,e.mount,e.modalRef,o.hiddenSiblings,!1),this.containers.splice(r,1);else{const l=o.modals[o.modals.length-1];l.modalRef&&a4e(l.modalRef,!1)}return i}isTopModal(e){return this.modals.length>0&&this.modals[this.modals.length-1]===e}}function Bfe(n){let e=n.activeElement;for(;e?.shadowRoot?.activeElement!=null;)e=e.shadowRoot.activeElement;return e}const P0r=["input","select","textarea","a[href]","button","[tabindex]","audio[controls]","video[controls]",'[contenteditable]:not([contenteditable="false"])'].join(",");function F0r(n){const e=parseInt(n.getAttribute("tabindex")||"",10);return Number.isNaN(e)?n.contentEditable==="true"||(n.nodeName==="AUDIO"||n.nodeName==="VIDEO"||n.nodeName==="DETAILS")&&n.getAttribute("tabindex")===null?0:n.tabIndex:e}function j0r(n){if(n.tagName!=="INPUT"||n.type!=="radio"||!n.name)return!1;const e=i=>n.ownerDocument.querySelector(`input[type="radio"]${i}`);let t=e(`[name="${n.name}"]:checked`);return t||(t=e(`[name="${n.name}"]`)),t!==n}function H0r(n){return!(n.disabled||n.tagName==="INPUT"&&n.type==="hidden"||j0r(n))}function B0r(n){const e=[],t=[];return Array.from(n.querySelectorAll(P0r)).forEach((i,r)=>{const o=F0r(i);o===-1||!H0r(i)||(o===0?e.push(i):t.push({documentOrder:r,tabIndex:o,node:i}))}),t.sort((i,r)=>i.tabIndex===r.tabIndex?i.documentOrder-r.documentOrder:i.tabIndex-r.tabIndex).map(i=>i.node).concat(e)}function W0r(){return!0}function bui(n){const{children:e,disableAutoFocus:t=!1,disableEnforceFocus:i=!1,disableRestoreFocus:r=!1,getTabbable:o=B0r,isEnabled:l=W0r,open:c}=n,d=D.useRef(!1),h=D.useRef(null),p=D.useRef(null),m=D.useRef(null),b=D.useRef(null),w=D.useRef(!1),_=D.useRef(null),x=xm(FY(e),_),T=D.useRef(null);D.useEffect(()=>{!c||!_.current||(w.current=!t)},[t,c]),D.useEffect(()=>{if(!c||!_.current)return;const A=hv(_.current),M=Bfe(A);return _.current.contains(M)||(_.current.hasAttribute("tabIndex")||_.current.setAttribute("tabIndex","-1"),w.current&&_.current.focus()),()=>{r||(m.current&&m.current.focus&&(d.current=!0,m.current.focus()),m.current=null)}},[c]),D.useEffect(()=>{if(!c||!_.current)return;const A=hv(_.current),M=j=>{if(T.current=j,i||!l()||j.key!=="Tab")return;Bfe(A)===_.current&&j.shiftKey&&(d.current=!0,p.current&&p.current.focus())},O=()=>{const j=_.current;if(j===null)return;const W=Bfe(A);if(!A.hasFocus()||!l()||d.current){d.current=!1;return}if(j.contains(W)||i&&W!==h.current&&W!==p.current)return;if(W!==b.current)b.current=null;else if(b.current!==null)return;if(!w.current)return;let q=[];if((W===h.current||W===p.current)&&(q=o(_.current)),q.length>0){const Z=!!(T.current?.shiftKey&&T.current?.key==="Tab"),ee=q[0],G=q[q.length-1];typeof ee!="string"&&typeof G!="string"&&(Z?G.focus():ee.focus())}else j.focus()};A.addEventListener("focusin",O),A.addEventListener("keydown",M,!0);const F=setInterval(()=>{const j=Bfe(A);j&&j.tagName==="BODY"&&O()},50);return()=>{clearInterval(F),A.removeEventListener("focusin",O),A.removeEventListener("keydown",M,!0)}},[t,i,r,l,c,o]);const I=A=>{m.current===null&&(m.current=A.relatedTarget),w.current=!0,b.current=A.target;const M=e.props.onFocus;M&&M(A)},L=A=>{m.current===null&&(m.current=A.relatedTarget),w.current=!0};return k.jsxs(D.Fragment,{children:[k.jsx("div",{tabIndex:c?0:-1,onFocus:L,ref:h,"data-testid":"sentinelStart"}),D.cloneElement(e,{ref:x,onFocus:I}),k.jsx("div",{tabIndex:c?0:-1,onFocus:L,ref:p,"data-testid":"sentinelEnd"})]})}function V0r(n){return typeof n=="function"?n():n}function $0r(n){return n?n.props.hasOwnProperty("in"):!1}const WPn=()=>{},NBe=new N0r;function z0r(n){const{container:e,disableEscapeKeyDown:t=!1,disableScrollLock:i=!1,closeAfterTransition:r=!1,onTransitionEnter:o,onTransitionExited:l,children:c,onClose:d,open:h,rootRef:p}=n,m=D.useRef({}),b=D.useRef(null),w=D.useRef(null),_=xm(w,p),[x,T]=D.useState(!h),I=$0r(c);let L=!0;(n["aria-hidden"]==="false"||n["aria-hidden"]===!1)&&(L=!1);const A=()=>hv(b.current),M=()=>(m.current.modalRef=w.current,m.current.mount=b.current,m.current),O=()=>{NBe.mount(M(),{disableScrollLock:i}),w.current&&(w.current.scrollTop=0)},F=ub(()=>{const ie=V0r(e)||A().body;NBe.add(M(),ie),w.current&&O()}),j=()=>NBe.isTopModal(M()),W=ub(ie=>{b.current=ie,ie&&(h&&j()?O():w.current&&a4e(w.current,L))}),q=D.useCallback(()=>{NBe.remove(M(),L)},[L]);D.useEffect(()=>()=>{q()},[q]),D.useEffect(()=>{h?F():(!I||!r)&&q()},[h,q,I,r,F]);const Z=ie=>se=>{ie.onKeyDown?.(se),!(se.key!=="Escape"||se.which===229||!j())&&(t||(se.stopPropagation(),d&&d(se,"escapeKeyDown")))},ee=ie=>se=>{ie.onClick?.(se),se.target===se.currentTarget&&d&&d(se,"backdropClick")};return{getRootProps:(ie={})=>{const se=vie(n);delete se.onTransitionEnter,delete se.onTransitionExited;const de={...se,...ie};return{role:"presentation",...de,onKeyDown:Z(de),ref:_}},getBackdropProps:(ie={})=>{const se=ie;return{"aria-hidden":!0,...se,onClick:ee(se),open:h}},getTransitionProps:()=>{const ie=()=>{T(!1),o&&o()},se=()=>{T(!0),l&&l(),r&&q()};return{onEnter:zOt(ie,c?.props.onEnter??WPn),onExited:zOt(se,c?.props.onExited??WPn)}},rootRef:_,portalRef:W,isTopModal:j,exited:x,hasTransition:I}}function U0r(n){return No("MuiModal",n)}Po("MuiModal",["root","hidden","backdrop"]);const q0r=n=>{const{open:e,exited:t,classes:i}=n;return Fo({root:["root",!e&&t&&"hidden"],backdrop:["backdrop"]},U0r,i)},G0r=tn("div",{name:"MuiModal",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,!t.open&&t.exited&&e.hidden]}})(Gs(({theme:n})=>({position:"fixed",zIndex:(n.vars||n).zIndex.modal,right:0,bottom:0,top:0,left:0,variants:[{props:({ownerState:e})=>!e.open&&e.exited,style:{visibility:"hidden"}}]}))),K0r=tn(lUt,{name:"MuiModal",slot:"Backdrop"})({zIndex:-1}),Yet=D.forwardRef(function(e,t){const i=Wo({name:"MuiModal",props:e}),{BackdropComponent:r=K0r,BackdropProps:o,classes:l,className:c,closeAfterTransition:d=!1,children:h,container:p,component:m,components:b={},componentsProps:w={},disableAutoFocus:_=!1,disableEnforceFocus:x=!1,disableEscapeKeyDown:T=!1,disablePortal:I=!1,disableRestoreFocus:L=!1,disableScrollLock:A=!1,hideBackdrop:M=!1,keepMounted:O=!1,onClose:F,onTransitionEnter:j,onTransitionExited:W,open:q,slotProps:Z={},slots:ee={},theme:G,...te}=i,Q={...i,closeAfterTransition:d,disableAutoFocus:_,disableEnforceFocus:x,disableEscapeKeyDown:T,disablePortal:I,disableRestoreFocus:L,disableScrollLock:A,hideBackdrop:M,keepMounted:O},{getRootProps:ie,getBackdropProps:se,getTransitionProps:de,portalRef:ne,isTopModal:we,exited:ue,hasTransition:ce}=z0r({...Q,rootRef:t}),ye={...Q,exited:ue},he=q0r(ye),pe={};if(h.props.tabIndex===void 0&&(pe.tabIndex="-1"),ce){const{onEnter:tt,onExited:Ue}=de();pe.onEnter=tt,pe.onExited=Ue}const me={slots:{root:b.Root,backdrop:b.Backdrop,...ee},slotProps:{...w,...Z}},[be,xe]=_o("root",{ref:t,elementType:G0r,externalForwardedProps:{...me,...te,component:m},getSlotProps:ie,ownerState:ye,className:_i(c,he?.root,!ye.open&&ye.exited&&he?.hidden)}),[Te,Ge]=_o("backdrop",{ref:o?.ref,elementType:r,externalForwardedProps:me,shouldForwardComponentProp:!0,additionalProps:o,getSlotProps:tt=>se({...tt,onClick:Ue=>{tt?.onClick&&tt.onClick(Ue)}}),className:_i(o?.className,he?.backdrop),ownerState:ye});return!O&&!q&&(!ce||ue)?null:k.jsx(uui,{ref:ne,container:p,disablePortal:I,children:k.jsxs(be,{...xe,children:[!M&&r?k.jsx(Te,{...Ge}):null,k.jsx(bui,{disableEnforceFocus:x,disableAutoFocus:_,disableRestoreFocus:L,isEnabled:we,open:q,children:D.cloneElement(h,pe)})]})})});function Y0r(n){return No("MuiDialog",n)}const l4e=Po("MuiDialog",["root","backdrop","scrollPaper","scrollBody","container","paper","paperScrollPaper","paperScrollBody","paperWidthFalse","paperWidthXs","paperWidthSm","paperWidthMd","paperWidthLg","paperWidthXl","paperFullWidth","paperFullScreen"]),vui=D.createContext({}),Z0r=tn(lUt,{name:"MuiDialog",slot:"Backdrop"})({zIndex:-1}),X0r=n=>{const{classes:e,scroll:t,maxWidth:i,fullWidth:r,fullScreen:o}=n,l={root:["root"],backdrop:["backdrop"],container:["container",`scroll${ii(t)}`],paper:["paper",`paperScroll${ii(t)}`,`paperWidth${ii(String(i))}`,r&&"paperFullWidth",o&&"paperFullScreen"]};return Fo(l,Y0r,e)},Q0r=tn(Yet,{name:"MuiDialog",slot:"Root"})({"@media print":{position:"absolute !important"}}),J0r=tn("div",{name:"MuiDialog",slot:"Container",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.container,e[`scroll${ii(t.scroll)}`]]}})({height:"100%","@media print":{height:"auto"},outline:0,variants:[{props:{scroll:"paper"},style:{display:"flex",justifyContent:"center",alignItems:"center"}},{props:{scroll:"body"},style:{overflowY:"auto",overflowX:"hidden",textAlign:"center","&::after":{content:'""',display:"inline-block",verticalAlign:"middle",height:"100%",width:"0"}}}]}),ebr=tn(Jf,{name:"MuiDialog",slot:"Paper",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.paper,e[`scrollPaper${ii(t.scroll)}`],e[`paperWidth${ii(String(t.maxWidth))}`],t.fullWidth&&e.paperFullWidth,t.fullScreen&&e.paperFullScreen]}})(Gs(({theme:n})=>({margin:32,position:"relative",overflowY:"auto","@media print":{overflowY:"visible",boxShadow:"none"},variants:[{props:{scroll:"paper"},style:{display:"flex",flexDirection:"column",maxHeight:"calc(100% - 64px)"}},{props:{scroll:"body"},style:{display:"inline-block",verticalAlign:"middle",textAlign:"initial"}},{props:({ownerState:e})=>!e.maxWidth,style:{maxWidth:"calc(100% - 64px)"}},{props:{maxWidth:"xs"},style:{maxWidth:n.breakpoints.unit==="px"?Math.max(n.breakpoints.values.xs,444):`max(${n.breakpoints.values.xs}${n.breakpoints.unit}, 444px)`,[`&.${l4e.paperScrollBody}`]:{[n.breakpoints.down(Math.max(n.breakpoints.values.xs,444)+64)]:{maxWidth:"calc(100% - 64px)"}}}},...Object.keys(n.breakpoints.values).filter(e=>e!=="xs").map(e=>({props:{maxWidth:e},style:{maxWidth:`${n.breakpoints.values[e]}${n.breakpoints.unit}`,[`&.${l4e.paperScrollBody}`]:{[n.breakpoints.down(n.breakpoints.values[e]+64)]:{maxWidth:"calc(100% - 64px)"}}}})),{props:({ownerState:e})=>e.fullWidth,style:{width:"calc(100% - 64px)"}},{props:({ownerState:e})=>e.fullScreen,style:{margin:0,width:"100%",maxWidth:"100%",height:"100%",maxHeight:"none",borderRadius:0,[`&.${l4e.paperScrollBody}`]:{margin:0,maxWidth:"100%"}}}]}))),fT=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiDialog"}),r=Lf(),o={enter:r.transitions.duration.enteringScreen,exit:r.transitions.duration.leavingScreen},{"aria-describedby":l,"aria-labelledby":c,"aria-modal":d=!0,BackdropComponent:h,BackdropProps:p,children:m,className:b,disableEscapeKeyDown:w=!1,fullScreen:_=!1,fullWidth:x=!1,maxWidth:T="sm",onClick:I,onClose:L,open:A,PaperComponent:M=Jf,PaperProps:O={},scroll:F="paper",slots:j={},slotProps:W={},TransitionComponent:q=eY,transitionDuration:Z=o,TransitionProps:ee,...G}=i,te={...i,disableEscapeKeyDown:w,fullScreen:_,fullWidth:x,maxWidth:T,scroll:F},Q=X0r(te),ie=D.useRef(),se=He=>{ie.current=He.target===He.currentTarget},de=He=>{I&&I(He),ie.current&&(ie.current=null,L&&L(He,"backdropClick"))},ne=YW(c),we=D.useMemo(()=>({titleId:ne}),[ne]),ue={transition:q,...j},ce={transition:ee,paper:O,backdrop:p,...W},ye={slots:ue,slotProps:ce},[he,pe]=_o("root",{elementType:Q0r,shouldForwardComponentProp:!0,externalForwardedProps:ye,ownerState:te,className:_i(Q.root,b),ref:t}),[me,be]=_o("backdrop",{elementType:Z0r,shouldForwardComponentProp:!0,externalForwardedProps:ye,ownerState:te,className:Q.backdrop}),[xe,Te]=_o("paper",{elementType:ebr,shouldForwardComponentProp:!0,externalForwardedProps:ye,ownerState:te,className:_i(Q.paper,O.className)}),[Ge,tt]=_o("container",{elementType:J0r,externalForwardedProps:ye,ownerState:te,className:Q.container}),[Ue,Me]=_o("transition",{elementType:eY,externalForwardedProps:ye,ownerState:te,additionalProps:{appear:!0,in:A,timeout:Z,role:"presentation"}});return k.jsx(he,{closeAfterTransition:!0,slots:{backdrop:me},slotProps:{backdrop:{transitionDuration:Z,as:h,...be}},disableEscapeKeyDown:w,onClose:L,open:A,onClick:de,...pe,...G,children:k.jsx(Ue,{...Me,children:k.jsx(Ge,{onMouseDown:se,...tt,children:k.jsx(xe,{as:M,elevation:24,role:"dialog","aria-describedby":l,"aria-labelledby":ne,"aria-modal":d,...Te,children:k.jsx(vui.Provider,{value:we,children:m})})})})})});function tbr(n){return No("MuiDialogActions",n)}Po("MuiDialogActions",["root","spacing"]);const nbr=n=>{const{classes:e,disableSpacing:t}=n;return Fo({root:["root",!t&&"spacing"]},tbr,e)},ibr=tn("div",{name:"MuiDialogActions",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,!t.disableSpacing&&e.spacing]}})({display:"flex",alignItems:"center",padding:8,justifyContent:"flex-end",flex:"0 0 auto",variants:[{props:({ownerState:n})=>!n.disableSpacing,style:{"& > :not(style) ~ :not(style)":{marginLeft:8}}}]}),$3=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiDialogActions"}),{className:r,disableSpacing:o=!1,...l}=i,c={...i,disableSpacing:o},d=nbr(c);return k.jsx(ibr,{className:_i(d.root,r),ownerState:c,ref:t,...l})});function rbr(n){return No("MuiDialogContent",n)}Po("MuiDialogContent",["root","dividers"]);function sbr(n){return No("MuiDialogTitle",n)}const obr=Po("MuiDialogTitle",["root"]),abr=n=>{const{classes:e,dividers:t}=n;return Fo({root:["root",t&&"dividers"]},rbr,e)},lbr=tn("div",{name:"MuiDialogContent",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.dividers&&e.dividers]}})(Gs(({theme:n})=>({flex:"1 1 auto",WebkitOverflowScrolling:"touch",overflowY:"auto",padding:"20px 24px",variants:[{props:({ownerState:e})=>e.dividers,style:{padding:"16px 24px",borderTop:`1px solid ${(n.vars||n).palette.divider}`,borderBottom:`1px solid ${(n.vars||n).palette.divider}`}},{props:({ownerState:e})=>!e.dividers,style:{[`.${obr.root} + &`]:{paddingTop:0}}}]}))),u8=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiDialogContent"}),{className:r,dividers:o=!1,...l}=i,c={...i,dividers:o},d=abr(c);return k.jsx(lbr,{className:_i(d.root,r),ownerState:c,ref:t,...l})}),cbr=n=>{const{classes:e}=n;return Fo({root:["root"]},sbr,e)},ubr=tn(di,{name:"MuiDialogTitle",slot:"Root"})({padding:"16px 24px",flex:"0 0 auto"}),z3=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiDialogTitle"}),{className:r,id:o,...l}=i,c=i,d=cbr(c),{titleId:h=o}=D.useContext(vui);return k.jsx(ubr,{component:"h2",className:_i(d.root,r),ownerState:c,ref:t,variant:"h6",id:o??h,...l})});function dbr(n){return No("MuiDivider",n)}const VPn=Po("MuiDivider",["root","absolute","fullWidth","inset","middle","flexItem","light","vertical","withChildren","withChildrenVertical","textAlignRight","textAlignLeft","wrapper","wrapperVertical"]),hbr=n=>{const{absolute:e,children:t,classes:i,flexItem:r,light:o,orientation:l,textAlign:c,variant:d}=n;return Fo({root:["root",e&&"absolute",d,o&&"light",l==="vertical"&&"vertical",r&&"flexItem",t&&"withChildren",t&&l==="vertical"&&"withChildrenVertical",c==="right"&&l!=="vertical"&&"textAlignRight",c==="left"&&l!=="vertical"&&"textAlignLeft"],wrapper:["wrapper",l==="vertical"&&"wrapperVertical"]},dbr,i)},fbr=tn("div",{name:"MuiDivider",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.absolute&&e.absolute,e[t.variant],t.light&&e.light,t.orientation==="vertical"&&e.vertical,t.flexItem&&e.flexItem,t.children&&e.withChildren,t.children&&t.orientation==="vertical"&&e.withChildrenVertical,t.textAlign==="right"&&t.orientation!=="vertical"&&e.textAlignRight,t.textAlign==="left"&&t.orientation!=="vertical"&&e.textAlignLeft]}})(Gs(({theme:n})=>({margin:0,flexShrink:0,borderWidth:0,borderStyle:"solid",borderColor:(n.vars||n).palette.divider,borderBottomWidth:"thin",variants:[{props:{absolute:!0},style:{position:"absolute",bottom:0,left:0,width:"100%"}},{props:{light:!0},style:{borderColor:n.alpha((n.vars||n).palette.divider,.08)}},{props:{variant:"inset"},style:{marginLeft:72}},{props:{variant:"middle",orientation:"horizontal"},style:{marginLeft:n.spacing(2),marginRight:n.spacing(2)}},{props:{variant:"middle",orientation:"vertical"},style:{marginTop:n.spacing(1),marginBottom:n.spacing(1)}},{props:{orientation:"vertical"},style:{height:"100%",borderBottomWidth:0,borderRightWidth:"thin"}},{props:{flexItem:!0},style:{alignSelf:"stretch",height:"auto"}},{props:({ownerState:e})=>!!e.children,style:{display:"flex",textAlign:"center",border:0,borderTopStyle:"solid",borderLeftStyle:"solid","&::before, &::after":{content:'""',alignSelf:"center"}}},{props:({ownerState:e})=>e.children&&e.orientation!=="vertical",style:{"&::before, &::after":{width:"100%",borderTop:`thin solid ${(n.vars||n).palette.divider}`,borderTopStyle:"inherit"}}},{props:({ownerState:e})=>e.orientation==="vertical"&&e.children,style:{flexDirection:"column","&::before, &::after":{height:"100%",borderLeft:`thin solid ${(n.vars||n).palette.divider}`,borderLeftStyle:"inherit"}}},{props:({ownerState:e})=>e.textAlign==="right"&&e.orientation!=="vertical",style:{"&::before":{width:"90%"},"&::after":{width:"10%"}}},{props:({ownerState:e})=>e.textAlign==="left"&&e.orientation!=="vertical",style:{"&::before":{width:"10%"},"&::after":{width:"90%"}}}]}))),pbr=tn("span",{name:"MuiDivider",slot:"Wrapper",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.wrapper,t.orientation==="vertical"&&e.wrapperVertical]}})(Gs(({theme:n})=>({display:"inline-block",paddingLeft:`calc(${n.spacing(1)} * 1.2)`,paddingRight:`calc(${n.spacing(1)} * 1.2)`,whiteSpace:"nowrap",variants:[{props:{orientation:"vertical"},style:{paddingTop:`calc(${n.spacing(1)} * 1.2)`,paddingBottom:`calc(${n.spacing(1)} * 1.2)`}}]}))),tY=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiDivider"}),{absolute:r=!1,children:o,className:l,orientation:c="horizontal",component:d=o||c==="vertical"?"div":"hr",flexItem:h=!1,light:p=!1,role:m=d!=="hr"?"separator":void 0,textAlign:b="center",variant:w="fullWidth",..._}=i,x={...i,absolute:r,component:d,flexItem:h,light:p,orientation:c,role:m,textAlign:b,variant:w},T=hbr(x);return k.jsx(fbr,{as:d,className:_i(T.root,l),role:m,ref:t,ownerState:x,"aria-orientation":m==="separator"&&(d!=="hr"||c==="vertical")?c:void 0,..._,children:o?k.jsx(pbr,{className:T.wrapper,ownerState:x,children:o}):null})});tY&&(tY.muiSkipListHighlight=!0);function gbr(n,e,t){const i=e.getBoundingClientRect(),r=t&&t.getBoundingClientRect(),o=Y6(e);let l;if(e.fakeTransform)l=e.fakeTransform;else{const h=o.getComputedStyle(e);l=h.getPropertyValue("-webkit-transform")||h.getPropertyValue("transform")}let c=0,d=0;if(l&&l!=="none"&&typeof l=="string"){const h=l.split("(")[1].split(")")[0].split(",");c=parseInt(h[4],10),d=parseInt(h[5],10)}return n==="left"?r?`translateX(${r.right+c-i.left}px)`:`translateX(${o.innerWidth+c-i.left}px)`:n==="right"?r?`translateX(-${i.right-r.left-c}px)`:`translateX(-${i.left+i.width-c}px)`:n==="up"?r?`translateY(${r.bottom+d-i.top}px)`:`translateY(${o.innerHeight+d-i.top}px)`:r?`translateY(-${i.top-r.top+i.height-d}px)`:`translateY(-${i.top+i.height-d}px)`}function mbr(n){return typeof n=="function"?n():n}function PBe(n,e,t){const i=mbr(t),r=gbr(n,e,i);r&&(e.style.webkitTransform=r,e.style.transform=r)}const bbr=D.forwardRef(function(e,t){const i=Lf(),r={enter:i.transitions.easing.easeOut,exit:i.transitions.easing.sharp},o={enter:i.transitions.duration.enteringScreen,exit:i.transitions.duration.leavingScreen},{addEndListener:l,appear:c=!0,children:d,container:h,direction:p="down",easing:m=r,in:b,onEnter:w,onEntered:_,onEntering:x,onExit:T,onExited:I,onExiting:L,style:A,timeout:M=o,TransitionComponent:O=l8,...F}=e,j=D.useRef(null),W=xm(FY(d),j,t),q=ne=>we=>{ne&&(we===void 0?ne(j.current):ne(j.current,we))},Z=q((ne,we)=>{PBe(p,ne,h),Yzt(ne),w&&w(ne,we)}),ee=q((ne,we)=>{const ue=QK({timeout:M,style:A,easing:m},{mode:"enter"});ne.style.webkitTransition=i.transitions.create("-webkit-transform",{...ue}),ne.style.transition=i.transitions.create("transform",{...ue}),ne.style.webkitTransform="none",ne.style.transform="none",x&&x(ne,we)}),G=q(_),te=q(L),Q=q(ne=>{const we=QK({timeout:M,style:A,easing:m},{mode:"exit"});ne.style.webkitTransition=i.transitions.create("-webkit-transform",we),ne.style.transition=i.transitions.create("transform",we),PBe(p,ne,h),T&&T(ne)}),ie=q(ne=>{ne.style.webkitTransition="",ne.style.transition="",I&&I(ne)}),se=ne=>{l&&l(j.current,ne)},de=D.useCallback(()=>{j.current&&PBe(p,j.current,h)},[p,h]);return D.useEffect(()=>{if(b||p==="down"||p==="right")return;const ne=g5e(()=>{j.current&&PBe(p,j.current,h)}),we=Y6(j.current);return we.addEventListener("resize",ne),()=>{ne.clear(),we.removeEventListener("resize",ne)}},[p,b,h]),D.useEffect(()=>{b||de()},[b,de]),k.jsx(O,{nodeRef:j,onEnter:Z,onEntered:G,onEntering:ee,onExit:Q,onExited:ie,onExiting:te,addEndListener:se,appear:c,in:b,timeout:M,...F,children:(ne,{ownerState:we,...ue})=>D.cloneElement(d,{ref:W,style:{visibility:ne==="exited"&&!b?"hidden":void 0,...A,...d.props.style},...ue})})});function vbr(n){return No("MuiDrawer",n)}Po("MuiDrawer",["root","docked","paper","anchorLeft","anchorRight","anchorTop","anchorBottom","paperAnchorLeft","paperAnchorRight","paperAnchorTop","paperAnchorBottom","paperAnchorDockedLeft","paperAnchorDockedRight","paperAnchorDockedTop","paperAnchorDockedBottom","modal"]);const wui=(n,e)=>{const{ownerState:t}=n;return[e.root,(t.variant==="permanent"||t.variant==="persistent")&&e.docked,t.variant==="temporary"&&e.modal]},wbr=n=>{const{classes:e,anchor:t,variant:i}=n,r={root:["root",`anchor${ii(t)}`],docked:[(i==="permanent"||i==="persistent")&&"docked"],modal:["modal"],paper:["paper",`paperAnchor${ii(t)}`,i!=="temporary"&&`paperAnchorDocked${ii(t)}`]};return Fo(r,vbr,e)},ybr=tn(Yet,{name:"MuiDrawer",slot:"Root",overridesResolver:wui})(Gs(({theme:n})=>({zIndex:(n.vars||n).zIndex.drawer}))),_br=tn("div",{shouldForwardProp:B_,name:"MuiDrawer",slot:"Docked",skipVariantsResolver:!1,overridesResolver:wui})({flex:"0 0 auto"}),Cbr=tn(Jf,{name:"MuiDrawer",slot:"Paper",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.paper,e[`paperAnchor${ii(t.anchor)}`],t.variant!=="temporary"&&e[`paperAnchorDocked${ii(t.anchor)}`]]}})(Gs(({theme:n})=>({overflowY:"auto",display:"flex",flexDirection:"column",height:"100%",flex:"1 0 auto",zIndex:(n.vars||n).zIndex.drawer,WebkitOverflowScrolling:"touch",position:"fixed",top:0,outline:0,variants:[{props:{anchor:"left"},style:{left:0}},{props:{anchor:"top"},style:{top:0,left:0,right:0,height:"auto",maxHeight:"100%"}},{props:{anchor:"right"},style:{right:0}},{props:{anchor:"bottom"},style:{top:"auto",left:0,bottom:0,right:0,height:"auto",maxHeight:"100%"}},{props:({ownerState:e})=>e.anchor==="left"&&e.variant!=="temporary",style:{borderRight:`1px solid ${(n.vars||n).palette.divider}`}},{props:({ownerState:e})=>e.anchor==="top"&&e.variant!=="temporary",style:{borderBottom:`1px solid ${(n.vars||n).palette.divider}`}},{props:({ownerState:e})=>e.anchor==="right"&&e.variant!=="temporary",style:{borderLeft:`1px solid ${(n.vars||n).palette.divider}`}},{props:({ownerState:e})=>e.anchor==="bottom"&&e.variant!=="temporary",style:{borderTop:`1px solid ${(n.vars||n).palette.divider}`}}]}))),yui={left:"right",right:"left",top:"down",bottom:"up"};function Sbr(n){return["left","right"].includes(n)}function xbr({direction:n},e){return n==="rtl"&&Sbr(e)?yui[e]:e}const Ebr=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiDrawer"}),r=Lf(),o=MY(),l={enter:r.transitions.duration.enteringScreen,exit:r.transitions.duration.leavingScreen},{anchor:c="left",BackdropProps:d,children:h,className:p,elevation:m=16,hideBackdrop:b=!1,ModalProps:{BackdropProps:w,..._}={},onClose:x,open:T=!1,PaperProps:I={},SlideProps:L,TransitionComponent:A,transitionDuration:M=l,variant:O="temporary",slots:F={},slotProps:j={},...W}=i,q=D.useRef(!1);D.useEffect(()=>{q.current=!0},[]);const Z=xbr({direction:o?"rtl":"ltr"},c),G={...i,anchor:c,elevation:m,open:T,variant:O,...W},te=wbr(G),Q={slots:{transition:A,...F},slotProps:{paper:I,transition:L,...j,backdrop:Uzt(j.backdrop||{...d,...w},{transitionDuration:M})}},[ie,se]=_o("root",{ref:t,elementType:ybr,className:_i(te.root,te.modal,p),shouldForwardComponentProp:!0,ownerState:G,externalForwardedProps:{...Q,...W,..._},additionalProps:{open:T,onClose:x,hideBackdrop:b,slots:{backdrop:Q.slots.backdrop},slotProps:{backdrop:Q.slotProps.backdrop}}}),[de,ne]=_o("paper",{elementType:Cbr,shouldForwardComponentProp:!0,className:_i(te.paper,I.className),ownerState:G,externalForwardedProps:Q,additionalProps:{elevation:O==="temporary"?m:0,square:!0,...O==="temporary"&&{role:"dialog","aria-modal":"true"}}}),[we,ue]=_o("docked",{elementType:_br,ref:t,className:_i(te.root,te.docked,p),ownerState:G,externalForwardedProps:Q,additionalProps:W}),[ce,ye]=_o("transition",{elementType:bbr,ownerState:G,externalForwardedProps:Q,additionalProps:{in:T,direction:yui[Z],timeout:M,appear:q.current}}),he=k.jsx(de,{...ne,children:h});if(O==="permanent")return k.jsx(we,{...ue,children:he});const pe=k.jsx(ce,{...ye,children:he});return O==="persistent"?k.jsx(we,{...ue,children:pe}):k.jsx(ie,{...se,children:pe})}),kbr=n=>{const{classes:e,disableUnderline:t,startAdornment:i,endAdornment:r,size:o,hiddenLabel:l,multiline:c}=n,d={root:["root",!t&&"underline",i&&"adornedStart",r&&"adornedEnd",o==="small"&&`size${ii(o)}`,l&&"hiddenLabel",c&&"multiline"],input:["input"]},h=Fo(d,d1r,e);return{...e,...h}},Tbr=tn(qet,{shouldForwardProp:n=>B_(n)||n==="classes",name:"MuiFilledInput",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[...zet(n,e),!t.disableUnderline&&e.underline]}})(Gs(({theme:n})=>{const e=n.palette.mode==="light",t=e?"rgba(0, 0, 0, 0.42)":"rgba(255, 255, 255, 0.7)",i=e?"rgba(0, 0, 0, 0.06)":"rgba(255, 255, 255, 0.09)",r=e?"rgba(0, 0, 0, 0.09)":"rgba(255, 255, 255, 0.13)",o=e?"rgba(0, 0, 0, 0.12)":"rgba(255, 255, 255, 0.12)";return{position:"relative",backgroundColor:n.vars?n.vars.palette.FilledInput.bg:i,borderTopLeftRadius:(n.vars||n).shape.borderRadius,borderTopRightRadius:(n.vars||n).shape.borderRadius,transition:n.transitions.create("background-color",{duration:n.transitions.duration.shorter,easing:n.transitions.easing.easeOut}),"&:hover":{backgroundColor:n.vars?n.vars.palette.FilledInput.hoverBg:r,"@media (hover: none)":{backgroundColor:n.vars?n.vars.palette.FilledInput.bg:i}},[`&.${v6.focused}`]:{backgroundColor:n.vars?n.vars.palette.FilledInput.bg:i},[`&.${v6.disabled}`]:{backgroundColor:n.vars?n.vars.palette.FilledInput.disabledBg:o},variants:[{props:({ownerState:l})=>!l.disableUnderline,style:{"&::after":{left:0,bottom:0,content:'""',position:"absolute",right:0,transform:"scaleX(0)",transition:n.transitions.create("transform",{duration:n.transitions.duration.shorter,easing:n.transitions.easing.easeOut}),pointerEvents:"none"},[`&.${v6.focused}:after`]:{transform:"scaleX(1) translateX(0)"},[`&.${v6.error}`]:{"&::before, &::after":{borderBottomColor:(n.vars||n).palette.error.main}},"&::before":{borderBottom:`1px solid ${n.vars?n.alpha(n.vars.palette.common.onBackground,n.vars.opacity.inputUnderline):t}`,left:0,bottom:0,content:'"\\00a0"',position:"absolute",right:0,transition:n.transitions.create("border-bottom-color",{duration:n.transitions.duration.shorter}),pointerEvents:"none"},[`&:hover:not(.${v6.disabled}, .${v6.error}):before`]:{borderBottom:`1px solid ${(n.vars||n).palette.text.primary}`},[`&.${v6.disabled}:before`]:{borderBottomStyle:"dotted"}}},...Object.entries(n.palette).filter(Vh()).map(([l])=>({props:{disableUnderline:!1,color:l},style:{"&::after":{borderBottom:`2px solid ${(n.vars||n).palette[l]?.main}`}}})),{props:({ownerState:l})=>l.startAdornment,style:{paddingLeft:12}},{props:({ownerState:l})=>l.endAdornment,style:{paddingRight:12}},{props:({ownerState:l})=>l.multiline,style:{padding:"25px 12px 8px"}},{props:({ownerState:l,size:c})=>l.multiline&&c==="small",style:{paddingTop:21,paddingBottom:4}},{props:({ownerState:l})=>l.multiline&&l.hiddenLabel,style:{paddingTop:16,paddingBottom:17}},{props:({ownerState:l})=>l.multiline&&l.hiddenLabel&&l.size==="small",style:{paddingTop:8,paddingBottom:9}}]}})),Lbr=tn(Get,{name:"MuiFilledInput",slot:"Input",overridesResolver:Uet})(Gs(({theme:n})=>({paddingTop:25,paddingRight:12,paddingBottom:8,paddingLeft:12,...!n.vars&&{"&:-webkit-autofill":{WebkitBoxShadow:n.palette.mode==="light"?null:"0 0 0 100px #266798 inset",WebkitTextFillColor:n.palette.mode==="light"?null:"#fff",caretColor:n.palette.mode==="light"?null:"#fff",borderTopLeftRadius:"inherit",borderTopRightRadius:"inherit"}},...n.vars&&{"&:-webkit-autofill":{borderTopLeftRadius:"inherit",borderTopRightRadius:"inherit"},[n.getColorSchemeSelector("dark")]:{"&:-webkit-autofill":{WebkitBoxShadow:"0 0 0 100px #266798 inset",WebkitTextFillColor:"#fff",caretColor:"#fff"}}},variants:[{props:{size:"small"},style:{paddingTop:21,paddingBottom:4}},{props:({ownerState:e})=>e.hiddenLabel,style:{paddingTop:16,paddingBottom:17}},{props:({ownerState:e})=>e.startAdornment,style:{paddingLeft:0}},{props:({ownerState:e})=>e.endAdornment,style:{paddingRight:0}},{props:({ownerState:e})=>e.hiddenLabel&&e.size==="small",style:{paddingTop:8,paddingBottom:9}},{props:({ownerState:e})=>e.multiline,style:{paddingTop:0,paddingBottom:0,paddingLeft:0,paddingRight:0}}]}))),uUt=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiFilledInput"}),{disableUnderline:r=!1,components:o={},componentsProps:l,fullWidth:c=!1,hiddenLabel:d,inputComponent:h="input",multiline:p=!1,slotProps:m,slots:b={},type:w="text",..._}=i,x={...i,disableUnderline:r,fullWidth:c,inputComponent:h,multiline:p,type:w},T=kbr(i),I={root:{ownerState:x},input:{ownerState:x}},L=m??l?O_(I,m??l):I,A=b.root??o.Root??Tbr,M=b.input??o.Input??Lbr;return k.jsx(Y1e,{slots:{root:A,input:M},slotProps:L,fullWidth:c,inputComponent:h,multiline:p,ref:t,type:w,..._,classes:T})});uUt.muiName="Input";function Dbr(n){return No("MuiFormControl",n)}Po("MuiFormControl",["root","marginNone","marginNormal","marginDense","fullWidth","disabled"]);const Ibr=n=>{const{classes:e,margin:t,fullWidth:i}=n,r={root:["root",t!=="none"&&`margin${ii(t)}`,i&&"fullWidth"]};return Fo(r,Dbr,e)},Abr=tn("div",{name:"MuiFormControl",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[`margin${ii(t.margin)}`],t.fullWidth&&e.fullWidth]}})({display:"inline-flex",flexDirection:"column",position:"relative",minWidth:0,padding:0,margin:0,border:0,verticalAlign:"top",variants:[{props:{margin:"normal"},style:{marginTop:16,marginBottom:8}},{props:{margin:"dense"},style:{marginTop:8,marginBottom:4}},{props:{fullWidth:!0},style:{width:"100%"}}]}),wW=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiFormControl"}),{children:r,className:o,color:l="primary",component:c="div",disabled:d=!1,error:h=!1,focused:p,fullWidth:m=!1,hiddenLabel:b=!1,margin:w="none",required:_=!1,size:x="medium",variant:T="outlined",...I}=i,L={...i,color:l,component:c,disabled:d,error:h,fullWidth:m,hiddenLabel:b,margin:w,required:_,size:x,variant:T},A=Ibr(L),[M,O]=D.useState(()=>{let ie=!1;return r&&D.Children.forEach(r,se=>{if(!n4e(se,["Input","Select"]))return;const de=n4e(se,["Select"])?se.props.input:se;de&&o1r(de.props)&&(ie=!0)}),ie}),[F,j]=D.useState(()=>{let ie=!1;return r&&D.Children.forEach(r,se=>{n4e(se,["Input","Select"])&&(oGe(se.props,!0)||oGe(se.props.inputProps,!0))&&(ie=!0)}),ie}),[W,q]=D.useState(!1);d&&W&&q(!1);const Z=p!==void 0&&!d?p:W;let ee;D.useRef(!1);const G=D.useCallback(()=>{j(!0)},[]),te=D.useCallback(()=>{j(!1)},[]),Q=D.useMemo(()=>({adornedStart:M,setAdornedStart:O,color:l,disabled:d,error:h,filled:F,focused:Z,fullWidth:m,hiddenLabel:b,size:x,onBlur:()=>{q(!1)},onFocus:()=>{q(!0)},onEmpty:te,onFilled:G,registerEffect:ee,required:_,variant:T}),[M,l,d,h,F,Z,m,b,ee,te,G,_,x,T]);return k.jsx($et.Provider,{value:Q,children:k.jsx(Abr,{as:c,ownerState:L,className:_i(A.root,o),ref:t,...I,children:r})})});function Rbr(n){return No("MuiFormControlLabel",n)}const pke=Po("MuiFormControlLabel",["root","labelPlacementStart","labelPlacementTop","labelPlacementBottom","disabled","label","error","required","asterisk"]),Mbr=n=>{const{classes:e,disabled:t,labelPlacement:i,error:r,required:o}=n,l={root:["root",t&&"disabled",`labelPlacement${ii(i)}`,r&&"error",o&&"required"],label:["label",t&&"disabled"],asterisk:["asterisk",r&&"error"]};return Fo(l,Rbr,e)},Obr=tn("label",{name:"MuiFormControlLabel",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[{[`& .${pke.label}`]:e.label},e.root,e[`labelPlacement${ii(t.labelPlacement)}`]]}})(Gs(({theme:n})=>({display:"inline-flex",alignItems:"center",cursor:"pointer",verticalAlign:"middle",WebkitTapHighlightColor:"transparent",marginLeft:-11,marginRight:16,[`&.${pke.disabled}`]:{cursor:"default"},[`& .${pke.label}`]:{[`&.${pke.disabled}`]:{color:(n.vars||n).palette.text.disabled}},variants:[{props:{labelPlacement:"start"},style:{flexDirection:"row-reverse",marginRight:-11}},{props:{labelPlacement:"top"},style:{flexDirection:"column-reverse"}},{props:{labelPlacement:"bottom"},style:{flexDirection:"column"}},{props:({labelPlacement:e})=>e==="start"||e==="top"||e==="bottom",style:{marginLeft:16}}]}))),Nbr=tn("span",{name:"MuiFormControlLabel",slot:"Asterisk"})(Gs(({theme:n})=>({[`&.${pke.error}`]:{color:(n.vars||n).palette.error.main}}))),j6=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiFormControlLabel"}),{checked:r,className:o,componentsProps:l={},control:c,disabled:d,disableTypography:h,inputRef:p,label:m,labelPlacement:b="end",name:w,onChange:_,required:x,slots:T={},slotProps:I={},value:L,...A}=i,M=DM(),O=d??c.props.disabled??M?.disabled,F=x??c.props.required,j={disabled:O,required:F};["checked","name","onChange","value","inputRef"].forEach(ie=>{typeof c.props[ie]>"u"&&typeof i[ie]<"u"&&(j[ie]=i[ie])});const W=jY({props:i,muiFormControl:M,states:["error"]}),q={...i,disabled:O,labelPlacement:b,required:F,error:W.error},Z=Mbr(q),ee={slots:T,slotProps:{...l,...I}},[G,te]=_o("typography",{elementType:di,externalForwardedProps:ee,ownerState:q});let Q=m;return Q!=null&&Q.type!==di&&!h&&(Q=k.jsx(G,{component:"span",...te,className:_i(Z.label,te?.className),children:Q})),k.jsxs(Obr,{className:_i(Z.root,o),ownerState:q,ref:t,...A,children:[D.cloneElement(c,j),F?k.jsxs("div",{children:[Q,k.jsxs(Nbr,{ownerState:q,"aria-hidden":!0,className:Z.asterisk,children:[" ","*"]})]}):Q]})});function Pbr(n){return No("MuiFormGroup",n)}Po("MuiFormGroup",["root","row","error"]);const Fbr=n=>{const{classes:e,row:t,error:i}=n;return Fo({root:["root",t&&"row",i&&"error"]},Pbr,e)},jbr=tn("div",{name:"MuiFormGroup",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.row&&e.row]}})({display:"flex",flexDirection:"column",flexWrap:"wrap",variants:[{props:{row:!0},style:{flexDirection:"row"}}]}),Hbr=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiFormGroup"}),{className:r,row:o=!1,...l}=i,c=DM(),d=jY({props:i,muiFormControl:c,states:["error"]}),h={...i,row:o,error:d.error},p=Fbr(h);return k.jsx(jbr,{className:_i(p.root,r),ownerState:h,ref:t,...l})});function Bbr(n){return No("MuiFormHelperText",n)}const $Pn=Po("MuiFormHelperText",["root","error","disabled","sizeSmall","sizeMedium","contained","focused","filled","required"]);var zPn;const Wbr=n=>{const{classes:e,contained:t,size:i,disabled:r,error:o,filled:l,focused:c,required:d}=n,h={root:["root",r&&"disabled",o&&"error",i&&`size${ii(i)}`,t&&"contained",c&&"focused",l&&"filled",d&&"required"]};return Fo(h,Bbr,e)},Vbr=tn("p",{name:"MuiFormHelperText",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.size&&e[`size${ii(t.size)}`],t.contained&&e.contained,t.filled&&e.filled]}})(Gs(({theme:n})=>({color:(n.vars||n).palette.text.secondary,...n.typography.caption,textAlign:"left",marginTop:3,marginRight:0,marginBottom:0,marginLeft:0,[`&.${$Pn.disabled}`]:{color:(n.vars||n).palette.text.disabled},[`&.${$Pn.error}`]:{color:(n.vars||n).palette.error.main},variants:[{props:{size:"small"},style:{marginTop:4}},{props:({ownerState:e})=>e.contained,style:{marginLeft:14,marginRight:14}}]}))),$br=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiFormHelperText"}),{children:r,className:o,component:l="p",disabled:c,error:d,filled:h,focused:p,margin:m,required:b,variant:w,..._}=i,x=DM(),T=jY({props:i,muiFormControl:x,states:["variant","size","disabled","error","filled","focused","required"]}),I={...i,component:l,contained:T.variant==="filled"||T.variant==="outlined",variant:T.variant,size:T.size,disabled:T.disabled,error:T.error,filled:T.filled,focused:T.focused,required:T.required};delete I.ownerState;const L=Wbr(I);return k.jsx(Vbr,{as:l,className:_i(L.root,o),ref:t,..._,ownerState:I,children:r===" "?zPn||(zPn=k.jsx("span",{className:"notranslate","aria-hidden":!0,children:"​"})):r})});function zbr(n){return No("MuiFormLabel",n)}const c4e=Po("MuiFormLabel",["root","colorSecondary","focused","disabled","error","filled","required","asterisk"]),Ubr=n=>{const{classes:e,color:t,focused:i,disabled:r,error:o,filled:l,required:c}=n,d={root:["root",`color${ii(t)}`,r&&"disabled",o&&"error",l&&"filled",i&&"focused",c&&"required"],asterisk:["asterisk",o&&"error"]};return Fo(d,zbr,e)},qbr=tn("label",{name:"MuiFormLabel",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.color==="secondary"&&e.colorSecondary,t.filled&&e.filled]}})(Gs(({theme:n})=>({color:(n.vars||n).palette.text.secondary,...n.typography.body1,lineHeight:"1.4375em",padding:0,position:"relative",variants:[...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{[`&.${c4e.focused}`]:{color:(n.vars||n).palette[e].main}}})),{props:{},style:{[`&.${c4e.disabled}`]:{color:(n.vars||n).palette.text.disabled},[`&.${c4e.error}`]:{color:(n.vars||n).palette.error.main}}}]}))),Gbr=tn("span",{name:"MuiFormLabel",slot:"Asterisk"})(Gs(({theme:n})=>({[`&.${c4e.error}`]:{color:(n.vars||n).palette.error.main}}))),Kbr=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiFormLabel"}),{children:r,className:o,color:l,component:c="label",disabled:d,error:h,filled:p,focused:m,required:b,...w}=i,_=DM(),x=jY({props:i,muiFormControl:_,states:["color","required","focused","disabled","error","filled"]}),T={...i,color:x.color||"primary",component:c,disabled:x.disabled,error:x.error,filled:x.filled,focused:x.focused,required:x.required},I=Ubr(T);return k.jsxs(qbr,{as:c,ownerState:T,className:_i(I.root,o),ref:t,...w,children:[r,x.required&&k.jsxs(Gbr,{ownerState:T,"aria-hidden":!0,className:I.asterisk,children:[" ","*"]})]})}),yi=ehr({createStyledComponent:tn("div",{name:"MuiGrid",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.container&&e.container]}}),componentName:"MuiGrid",useThemeProps:n=>Wo({props:n,name:"MuiGrid"}),useTheme:Lf});function eNt(n){return`scale(${n}, ${n**2})`}const Ybr={entering:{opacity:1,transform:eNt(1)},entered:{opacity:1,transform:"none"}},BEt=typeof navigator<"u"&&/^((?!chrome|android).)*(safari|mobile)/i.test(navigator.userAgent)&&/(os |version\/)15(.|_)4/i.test(navigator.userAgent),yW=D.forwardRef(function(e,t){const{addEndListener:i,appear:r=!0,children:o,easing:l,in:c,onEnter:d,onEntered:h,onEntering:p,onExit:m,onExited:b,onExiting:w,style:_,timeout:x="auto",TransitionComponent:T=l8,...I}=e,L=NG(),A=D.useRef(),M=Lf(),O=D.useRef(null),F=xm(O,FY(o),t),j=ie=>se=>{if(ie){const de=O.current;se===void 0?ie(de):ie(de,se)}},W=j(p),q=j((ie,se)=>{Yzt(ie);const{duration:de,delay:ne,easing:we}=QK({style:_,timeout:x,easing:l},{mode:"enter"});let ue;x==="auto"?(ue=M.transitions.getAutoHeightDuration(ie.clientHeight),A.current=ue):ue=de,ie.style.transition=[M.transitions.create("opacity",{duration:ue,delay:ne}),M.transitions.create("transform",{duration:BEt?ue:ue*.666,delay:ne,easing:we})].join(","),d&&d(ie,se)}),Z=j(h),ee=j(w),G=j(ie=>{const{duration:se,delay:de,easing:ne}=QK({style:_,timeout:x,easing:l},{mode:"exit"});let we;x==="auto"?(we=M.transitions.getAutoHeightDuration(ie.clientHeight),A.current=we):we=se,ie.style.transition=[M.transitions.create("opacity",{duration:we,delay:de}),M.transitions.create("transform",{duration:BEt?we:we*.666,delay:BEt?de:de||we*.333,easing:ne})].join(","),ie.style.opacity=0,ie.style.transform=eNt(.75),m&&m(ie)}),te=j(b),Q=ie=>{x==="auto"&&L.start(A.current||0,ie),i&&i(O.current,ie)};return k.jsx(T,{appear:r,in:c,nodeRef:O,onEnter:q,onEntered:Z,onEntering:W,onExit:G,onExited:te,onExiting:ee,addEndListener:Q,timeout:x==="auto"?null:x,...I,children:(ie,{ownerState:se,...de})=>D.cloneElement(o,{style:{opacity:0,transform:eNt(.75),visibility:ie==="exited"&&!c?"hidden":void 0,...Ybr[ie],..._,...o.props.style},ref:F,...de})})});yW&&(yW.muiSupportAuto=!0);const Zbr=n=>{const{classes:e,disableUnderline:t}=n,r=Fo({root:["root",!t&&"underline"],input:["input"]},c1r,e);return{...e,...r}},Xbr=tn(qet,{shouldForwardProp:n=>B_(n)||n==="classes",name:"MuiInput",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[...zet(n,e),!t.disableUnderline&&e.underline]}})(Gs(({theme:n})=>{let t=n.palette.mode==="light"?"rgba(0, 0, 0, 0.42)":"rgba(255, 255, 255, 0.7)";return n.vars&&(t=n.alpha(n.vars.palette.common.onBackground,n.vars.opacity.inputUnderline)),{position:"relative",variants:[{props:({ownerState:i})=>i.formControl,style:{"label + &":{marginTop:16}}},{props:({ownerState:i})=>!i.disableUnderline,style:{"&::after":{left:0,bottom:0,content:'""',position:"absolute",right:0,transform:"scaleX(0)",transition:n.transitions.create("transform",{duration:n.transitions.duration.shorter,easing:n.transitions.easing.easeOut}),pointerEvents:"none"},[`&.${yG.focused}:after`]:{transform:"scaleX(1) translateX(0)"},[`&.${yG.error}`]:{"&::before, &::after":{borderBottomColor:(n.vars||n).palette.error.main}},"&::before":{borderBottom:`1px solid ${t}`,left:0,bottom:0,content:'"\\00a0"',position:"absolute",right:0,transition:n.transitions.create("border-bottom-color",{duration:n.transitions.duration.shorter}),pointerEvents:"none"},[`&:hover:not(.${yG.disabled}, .${yG.error}):before`]:{borderBottom:`2px solid ${(n.vars||n).palette.text.primary}`,"@media (hover: none)":{borderBottom:`1px solid ${t}`}},[`&.${yG.disabled}:before`]:{borderBottomStyle:"dotted"}}},...Object.entries(n.palette).filter(Vh()).map(([i])=>({props:{color:i,disableUnderline:!1},style:{"&::after":{borderBottom:`2px solid ${(n.vars||n).palette[i].main}`}}}))]}})),Qbr=tn(Get,{name:"MuiInput",slot:"Input",overridesResolver:Uet})({}),dUt=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiInput"}),{disableUnderline:r=!1,components:o={},componentsProps:l,fullWidth:c=!1,inputComponent:d="input",multiline:h=!1,slotProps:p,slots:m={},type:b="text",...w}=i,_=Zbr(i),T={root:{ownerState:{disableUnderline:r}}},I=p??l?O_(p??l,T):T,L=m.root??o.Root??Xbr,A=m.input??o.Input??Qbr;return k.jsx(Y1e,{slots:{root:L,input:A},slotProps:I,fullWidth:c,inputComponent:d,multiline:h,ref:t,type:b,...w,classes:_})});dUt.muiName="Input";function Jbr(n){return No("MuiInputAdornment",n)}const UPn=Po("MuiInputAdornment",["root","filled","standard","outlined","positionStart","positionEnd","disablePointerEvents","hiddenLabel","sizeSmall"]);var qPn;const evr=(n,e)=>{const{ownerState:t}=n;return[e.root,e[`position${ii(t.position)}`],t.disablePointerEvents===!0&&e.disablePointerEvents,e[t.variant]]},tvr=n=>{const{classes:e,disablePointerEvents:t,hiddenLabel:i,position:r,size:o,variant:l}=n,c={root:["root",t&&"disablePointerEvents",r&&`position${ii(r)}`,l,i&&"hiddenLabel",o&&`size${ii(o)}`]};return Fo(c,Jbr,e)},nvr=tn("div",{name:"MuiInputAdornment",slot:"Root",overridesResolver:evr})(Gs(({theme:n})=>({display:"flex",maxHeight:"2em",alignItems:"center",whiteSpace:"nowrap",color:(n.vars||n).palette.action.active,variants:[{props:{variant:"filled"},style:{[`&.${UPn.positionStart}&:not(.${UPn.hiddenLabel})`]:{marginTop:16}}},{props:{position:"start"},style:{marginRight:8}},{props:{position:"end"},style:{marginLeft:8}},{props:{disablePointerEvents:!0},style:{pointerEvents:"none"}}]}))),y5e=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiInputAdornment"}),{children:r,className:o,component:l="div",disablePointerEvents:c=!1,disableTypography:d=!1,position:h,variant:p,...m}=i,b=DM()||{};let w=p;p&&b.variant,b&&!w&&(w=b.variant);const _={...i,hiddenLabel:b.hiddenLabel,size:b.size,disablePointerEvents:c,position:h,variant:w},x=tvr(_);return k.jsx($et.Provider,{value:null,children:k.jsx(nvr,{as:l,ownerState:_,className:_i(x.root,o),ref:t,...m,children:typeof r=="string"&&!d?k.jsx(di,{color:"textSecondary",children:r}):k.jsxs(D.Fragment,{children:[h==="start"?qPn||(qPn=k.jsx("span",{className:"notranslate","aria-hidden":!0,children:"​"})):null,r]})})})});function ivr(n){return No("MuiInputLabel",n)}Po("MuiInputLabel",["root","focused","disabled","error","required","asterisk","formControl","sizeSmall","shrink","animated","standard","filled","outlined"]);const rvr=n=>{const{classes:e,formControl:t,size:i,shrink:r,disableAnimation:o,variant:l,required:c}=n,d={root:["root",t&&"formControl",!o&&"animated",r&&"shrink",i&&i!=="medium"&&`size${ii(i)}`,l],asterisk:[c&&"asterisk"]},h=Fo(d,ivr,e);return{...e,...h}},svr=tn(Kbr,{shouldForwardProp:n=>B_(n)||n==="classes",name:"MuiInputLabel",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[{[`& .${c4e.asterisk}`]:e.asterisk},e.root,t.formControl&&e.formControl,t.size==="small"&&e.sizeSmall,t.shrink&&e.shrink,!t.disableAnimation&&e.animated,t.focused&&e.focused,e[t.variant]]}})(Gs(({theme:n})=>({display:"block",transformOrigin:"top left",whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:"100%",variants:[{props:({ownerState:e})=>e.formControl,style:{position:"absolute",left:0,top:0,transform:"translate(0, 20px) scale(1)"}},{props:{size:"small"},style:{transform:"translate(0, 17px) scale(1)"}},{props:({ownerState:e})=>e.shrink,style:{transform:"translate(0, -1.5px) scale(0.75)",transformOrigin:"top left",maxWidth:"133%"}},{props:({ownerState:e})=>!e.disableAnimation,style:{transition:n.transitions.create(["color","transform","max-width"],{duration:n.transitions.duration.shorter,easing:n.transitions.easing.easeOut})}},{props:{variant:"filled"},style:{zIndex:1,pointerEvents:"none",transform:"translate(12px, 16px) scale(1)",maxWidth:"calc(100% - 24px)"}},{props:{variant:"filled",size:"small"},style:{transform:"translate(12px, 13px) scale(1)"}},{props:({variant:e,ownerState:t})=>e==="filled"&&t.shrink,style:{userSelect:"none",pointerEvents:"auto",transform:"translate(12px, 7px) scale(0.75)",maxWidth:"calc(133% - 24px)"}},{props:({variant:e,ownerState:t,size:i})=>e==="filled"&&t.shrink&&i==="small",style:{transform:"translate(12px, 4px) scale(0.75)"}},{props:{variant:"outlined"},style:{zIndex:1,pointerEvents:"none",transform:"translate(14px, 16px) scale(1)",maxWidth:"calc(100% - 24px)"}},{props:{variant:"outlined",size:"small"},style:{transform:"translate(14px, 9px) scale(1)"}},{props:({variant:e,ownerState:t})=>e==="outlined"&&t.shrink,style:{userSelect:"none",pointerEvents:"auto",maxWidth:"calc(133% - 32px)",transform:"translate(14px, -9px) scale(0.75)"}}]}))),tse=D.forwardRef(function(e,t){const i=Wo({name:"MuiInputLabel",props:e}),{disableAnimation:r=!1,margin:o,shrink:l,variant:c,className:d,...h}=i,p=DM();let m=l;typeof m>"u"&&p&&(m=p.filled||p.focused||p.adornedStart);const b=jY({props:i,muiFormControl:p,states:["size","variant","required","focused"]}),w={...i,disableAnimation:r,formControl:p,shrink:m,size:b.size,variant:b.variant,required:b.required,focused:b.focused},_=rvr(w);return k.jsx(svr,{"data-shrink":m,ref:t,className:_i(_.root,d),...h,ownerState:w,classes:_})});function ovr(n){return No("MuiLinearProgress",n)}Po("MuiLinearProgress",["root","colorPrimary","colorSecondary","determinate","indeterminate","buffer","query","dashed","dashedColorPrimary","dashedColorSecondary","bar","bar1","bar2","barColorPrimary","barColorSecondary","bar1Indeterminate","bar1Determinate","bar1Buffer","bar2Indeterminate","bar2Buffer"]);const tNt=4,nNt=W3` +`,igr=typeof GOt!="string"?W1e` + animation: ${GOt} 1.4s linear infinite; + `:null,rgr=typeof KOt!="string"?W1e` + animation: ${KOt} 1.4s ease-in-out infinite; + `:null,sgr=n=>{const{classes:e,variant:t,color:i,disableShrink:r}=n,o={root:["root",t,`color${ri(i)}`],svg:["svg"],track:["track"],circle:["circle",`circle${ri(t)}`,r&&"circleDisableShrink"]};return jo(o,ngr,e)},ogr=nn("span",{name:"MuiCircularProgress",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.variant],e[`color${ri(t.color)}`]]}})(Gs(({theme:n})=>({display:"inline-block",variants:[{props:{variant:"determinate"},style:{transition:n.transitions.create("transform")}},{props:{variant:"indeterminate"},style:igr||{animation:`${GOt} 1.4s linear infinite`}},...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{color:(n.vars||n).palette[e].main}}))]}))),agr=nn("svg",{name:"MuiCircularProgress",slot:"Svg"})({display:"block"}),lgr=nn("circle",{name:"MuiCircularProgress",slot:"Circle",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.circle,e[`circle${ri(t.variant)}`],t.disableShrink&&e.circleDisableShrink]}})(Gs(({theme:n})=>({stroke:"currentColor",variants:[{props:{variant:"determinate"},style:{transition:n.transitions.create("stroke-dashoffset")}},{props:{variant:"indeterminate"},style:{strokeDasharray:"80px, 200px",strokeDashoffset:0}},{props:({ownerState:e})=>e.variant==="indeterminate"&&!e.disableShrink,style:rgr||{animation:`${KOt} 1.4s ease-in-out infinite`}}]}))),cgr=nn("circle",{name:"MuiCircularProgress",slot:"Track"})(Gs(({theme:n})=>({stroke:"currentColor",opacity:(n.vars||n).palette.action.activatedOpacity}))),N1=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiCircularProgress"}),{className:r,color:o="primary",disableShrink:l=!1,enableTrackSlot:c=!1,size:d=40,style:h,thickness:p=3.6,value:m=0,variant:b="indeterminate",...w}=i,_={...i,color:o,disableShrink:l,size:d,thickness:p,value:m,variant:b,enableTrackSlot:c},x=sgr(_),T={},I={},D={};if(b==="determinate"){const A=2*Math.PI*((vR-p)/2);T.strokeDasharray=A.toFixed(3),D["aria-valuenow"]=Math.round(m),T.strokeDashoffset=`${((100-m)/100*A).toFixed(3)}px`,I.transform="rotate(-90deg)"}return k.jsx(ogr,{className:_i(x.root,r),style:{width:d,height:d,...I,...h},ownerState:_,ref:t,role:"progressbar",...D,...w,children:k.jsxs(agr,{className:x.svg,ownerState:_,viewBox:`${vR/2} ${vR/2} ${vR} ${vR}`,children:[c?k.jsx(cgr,{className:x.track,ownerState:_,cx:vR,cy:vR,r:(vR-p)/2,fill:"none",strokeWidth:p,"aria-hidden":"true"}):null,k.jsx(lgr,{className:x.circle,style:T,ownerState:_,cx:vR,cy:vR,r:(vR-p)/2,fill:"none",strokeWidth:p})]})})});function ugr(n){return Po("MuiIconButton",n)}const hPn=Fo("MuiIconButton",["root","disabled","colorInherit","colorPrimary","colorSecondary","colorError","colorInfo","colorSuccess","colorWarning","edgeStart","edgeEnd","sizeSmall","sizeMedium","sizeLarge","loading","loadingIndicator","loadingWrapper"]),dgr=n=>{const{classes:e,disabled:t,color:i,edge:r,size:o,loading:l}=n,c={root:["root",l&&"loading",t&&"disabled",i!=="default"&&`color${ri(i)}`,r&&`edge${ri(r)}`,`size${ri(o)}`],loadingIndicator:["loadingIndicator"],loadingWrapper:["loadingWrapper"]};return jo(c,ugr,e)},hgr=nn(D3,{name:"MuiIconButton",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.loading&&e.loading,t.color!=="default"&&e[`color${ri(t.color)}`],t.edge&&e[`edge${ri(t.edge)}`],e[`size${ri(t.size)}`]]}})(Gs(({theme:n})=>({textAlign:"center",flex:"0 0 auto",fontSize:n.typography.pxToRem(24),padding:8,borderRadius:"50%",color:(n.vars||n).palette.action.active,transition:n.transitions.create("background-color",{duration:n.transitions.duration.shortest}),variants:[{props:e=>!e.disableRipple,style:{"--IconButton-hoverBg":n.alpha((n.vars||n).palette.action.active,(n.vars||n).palette.action.hoverOpacity),"&:hover":{backgroundColor:"var(--IconButton-hoverBg)","@media (hover: none)":{backgroundColor:"transparent"}}}},{props:{edge:"start"},style:{marginLeft:-12}},{props:{edge:"start",size:"small"},style:{marginLeft:-3}},{props:{edge:"end"},style:{marginRight:-12}},{props:{edge:"end",size:"small"},style:{marginRight:-3}}]})),Gs(({theme:n})=>({variants:[{props:{color:"inherit"},style:{color:"inherit"}},...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{color:(n.vars||n).palette[e].main}})),...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{"--IconButton-hoverBg":n.alpha((n.vars||n).palette[e].main,(n.vars||n).palette.action.hoverOpacity)}})),{props:{size:"small"},style:{padding:5,fontSize:n.typography.pxToRem(18)}},{props:{size:"large"},style:{padding:12,fontSize:n.typography.pxToRem(28)}}],[`&.${hPn.disabled}`]:{backgroundColor:"transparent",color:(n.vars||n).palette.action.disabled},[`&.${hPn.loading}`]:{color:"transparent"}}))),fgr=nn("span",{name:"MuiIconButton",slot:"LoadingIndicator"})(({theme:n})=>({display:"none",position:"absolute",visibility:"visible",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:(n.vars||n).palette.action.disabled,variants:[{props:{loading:!0},style:{display:"flex"}}]})),Lo=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiIconButton"}),{edge:r=!1,children:o,className:l,color:c="default",disabled:d=!1,disableFocusRipple:h=!1,size:p="medium",id:m,loading:b=null,loadingIndicator:w,..._}=i,x=KW(m),T=w??k.jsx(N1,{"aria-labelledby":x,color:"inherit",size:16}),I={...i,edge:r,color:c,disabled:d,disableFocusRipple:h,loading:b,loadingIndicator:T,size:p},D=dgr(I);return k.jsxs(hgr,{id:b?x:m,className:_i(D.root,l),centerRipple:!0,focusRipple:!h,disabled:d||b,ref:t,..._,ownerState:I,children:[typeof b=="boolean"&&k.jsx("span",{className:D.loadingWrapper,style:{display:"contents"},children:k.jsx(fgr,{className:D.loadingIndicator,ownerState:I,children:b&&T})}),o]})}),pgr=Ya(k.jsx("path",{d:"M20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4C12.76,4 13.5,4.11 14.2, 4.31L15.77,2.74C14.61,2.26 13.34,2 12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0, 0 22,12M7.91,10.08L6.5,11.5L11,16L21,6L19.59,4.58L11,13.17L7.91,10.08Z"})),ggr=Ya(k.jsx("path",{d:"M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z"})),mgr=Ya(k.jsx("path",{d:"M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"})),bgr=Ya(k.jsx("path",{d:"M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20, 12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10, 10 0 0,0 12,2M11,17H13V11H11V17Z"})),iui=Ya(k.jsx("path",{d:"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"})),vgr=n=>{const{variant:e,color:t,severity:i,classes:r}=n,o={root:["root",`color${ri(t||i)}`,`${e}${ri(t||i)}`,`${e}`],icon:["icon"],message:["message"],action:["action"]};return jo(o,tgr,r)},wgr=nn(Qf,{name:"MuiAlert",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.variant],e[`${t.variant}${ri(t.color||t.severity)}`]]}})(Gs(({theme:n})=>{const e=n.palette.mode==="light"?n.darken:n.lighten,t=n.palette.mode==="light"?n.lighten:n.darken;return{...n.typography.body2,backgroundColor:"transparent",display:"flex",padding:"6px 16px",variants:[...Object.entries(n.palette).filter(Vh(["light"])).map(([i])=>({props:{colorSeverity:i,variant:"standard"},style:{color:n.vars?n.vars.palette.Alert[`${i}Color`]:e(n.palette[i].light,.6),backgroundColor:n.vars?n.vars.palette.Alert[`${i}StandardBg`]:t(n.palette[i].light,.9),[`& .${dPn.icon}`]:n.vars?{color:n.vars.palette.Alert[`${i}IconColor`]}:{color:n.palette[i].main}}})),...Object.entries(n.palette).filter(Vh(["light"])).map(([i])=>({props:{colorSeverity:i,variant:"outlined"},style:{color:n.vars?n.vars.palette.Alert[`${i}Color`]:e(n.palette[i].light,.6),border:`1px solid ${(n.vars||n).palette[i].light}`,[`& .${dPn.icon}`]:n.vars?{color:n.vars.palette.Alert[`${i}IconColor`]}:{color:n.palette[i].main}}})),...Object.entries(n.palette).filter(Vh(["dark"])).map(([i])=>({props:{colorSeverity:i,variant:"filled"},style:{fontWeight:n.typography.fontWeightMedium,...n.vars?{color:n.vars.palette.Alert[`${i}FilledColor`],backgroundColor:n.vars.palette.Alert[`${i}FilledBg`]}:{backgroundColor:n.palette.mode==="dark"?n.palette[i].dark:n.palette[i].main,color:n.palette.getContrastText(n.palette[i].main)}}}))]}})),ygr=nn("div",{name:"MuiAlert",slot:"Icon"})({marginRight:12,padding:"7px 0",display:"flex",fontSize:22,opacity:.9}),_gr=nn("div",{name:"MuiAlert",slot:"Message"})({padding:"8px 0",minWidth:0,overflow:"auto"}),Cgr=nn("div",{name:"MuiAlert",slot:"Action"})({display:"flex",alignItems:"flex-start",padding:"4px 0 0 16px",marginLeft:"auto",marginRight:-8}),fPn={success:k.jsx(pgr,{fontSize:"inherit"}),warning:k.jsx(ggr,{fontSize:"inherit"}),error:k.jsx(mgr,{fontSize:"inherit"}),info:k.jsx(bgr,{fontSize:"inherit"})},Vet=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiAlert"}),{action:r,children:o,className:l,closeText:c="Close",color:d,components:h={},componentsProps:p={},icon:m,iconMapping:b=fPn,onClose:w,role:_="alert",severity:x="success",slotProps:T={},slots:I={},variant:D="standard",...A}=i,M={...i,color:d,severity:x,variant:D,colorSeverity:d||x},O=vgr(M),F={slots:{closeButton:h.CloseButton,closeIcon:h.CloseIcon,...I},slotProps:{...p,...T}},[j,W]=_o("root",{ref:t,shouldForwardComponentProp:!0,className:_i(O.root,l),elementType:wgr,externalForwardedProps:{...F,...A},ownerState:M,additionalProps:{role:_,elevation:0}}),[U,Z]=_o("icon",{className:O.icon,elementType:ygr,externalForwardedProps:F,ownerState:M}),[te,G]=_o("message",{className:O.message,elementType:_gr,externalForwardedProps:F,ownerState:M}),[ee,Q]=_o("action",{className:O.action,elementType:Cgr,externalForwardedProps:F,ownerState:M}),[ie,se]=_o("closeButton",{elementType:Lo,externalForwardedProps:F,ownerState:M}),[ue,ne]=_o("closeIcon",{elementType:iui,externalForwardedProps:F,ownerState:M});return k.jsxs(j,{...W,children:[m!==!1?k.jsx(U,{...Z,children:m||b[x]||fPn[x]}):null,k.jsx(te,{...G,children:o}),r!=null?k.jsx(ee,{...Q,children:r}):null,r==null&&w?k.jsx(ee,{...Q,children:k.jsx(ie,{size:"small","aria-label":c,title:c,color:"inherit",onClick:w,...se,children:k.jsx(ue,{fontSize:"small",...ne})})}):null]})});function Sgr(n){return Po("MuiTypography",n)}const pPn=Fo("MuiTypography",["root","h1","h2","h3","h4","h5","h6","subtitle1","subtitle2","body1","body2","inherit","button","caption","overline","alignLeft","alignRight","alignCenter","alignJustify","noWrap","gutterBottom","paragraph"]),xgr={primary:!0,secondary:!0,error:!0,info:!0,success:!0,warning:!0,textPrimary:!0,textSecondary:!0,textDisabled:!0},Egr=Uhr(),kgr=n=>{const{align:e,gutterBottom:t,noWrap:i,paragraph:r,variant:o,classes:l}=n,c={root:["root",o,n.align!=="inherit"&&`align${ri(e)}`,t&&"gutterBottom",i&&"noWrap",r&&"paragraph"]};return jo(c,Sgr,l)},Tgr=nn("span",{name:"MuiTypography",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.variant&&e[t.variant],t.align!=="inherit"&&e[`align${ri(t.align)}`],t.noWrap&&e.noWrap,t.gutterBottom&&e.gutterBottom,t.paragraph&&e.paragraph]}})(Gs(({theme:n})=>({margin:0,variants:[{props:{variant:"inherit"},style:{font:"inherit",lineHeight:"inherit",letterSpacing:"inherit"}},...Object.entries(n.typography).filter(([e,t])=>e!=="inherit"&&t&&typeof t=="object").map(([e,t])=>({props:{variant:e},style:t})),...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{color:(n.vars||n).palette[e].main}})),...Object.entries(n.palette?.text||{}).filter(([,e])=>typeof e=="string").map(([e])=>({props:{color:`text${ri(e)}`},style:{color:(n.vars||n).palette.text[e]}})),{props:({ownerState:e})=>e.align!=="inherit",style:{textAlign:"var(--Typography-textAlign)"}},{props:({ownerState:e})=>e.noWrap,style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}},{props:({ownerState:e})=>e.gutterBottom,style:{marginBottom:"0.35em"}},{props:({ownerState:e})=>e.paragraph,style:{marginBottom:16}}]}))),gPn={h1:"h1",h2:"h2",h3:"h3",h4:"h4",h5:"h5",h6:"h6",subtitle1:"h6",subtitle2:"h6",body1:"p",body2:"p",inherit:"p"},ei=L.forwardRef(function(e,t){const{color:i,...r}=Vo({props:e,name:"MuiTypography"}),o=!xgr[i],l=Egr({...r,...o&&{color:i}}),{align:c="inherit",className:d,component:h,gutterBottom:p=!1,noWrap:m=!1,paragraph:b=!1,variant:w="body1",variantMapping:_=gPn,...x}=l,T={...l,align:c,color:i,className:d,component:h,gutterBottom:p,noWrap:m,paragraph:b,variant:w,variantMapping:_},I=h||(b?"p":_[w]||gPn[w])||"span",D=kgr(T);return k.jsx(Tgr,{as:I,ref:t,className:_i(D.root,d),...x,ownerState:T,style:{...c!=="inherit"&&{"--Typography-textAlign":c},...x.style}})});function Lgr(n){return Po("MuiAlertTitle",n)}Fo("MuiAlertTitle",["root"]);const Dgr=n=>{const{classes:e}=n;return jo({root:["root"]},Lgr,e)},Igr=nn(ei,{name:"MuiAlertTitle",slot:"Root"})(Gs(({theme:n})=>({fontWeight:n.typography.fontWeightMedium,marginTop:-2}))),Agr=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiAlertTitle"}),{className:r,...o}=i,l=i,c=Dgr(l);return k.jsx(Igr,{gutterBottom:!0,component:"div",ownerState:l,ref:t,className:_i(c.root,r),...o})});function Rgr(n){return Po("MuiAppBar",n)}Fo("MuiAppBar",["root","positionFixed","positionAbsolute","positionSticky","positionStatic","positionRelative","colorDefault","colorPrimary","colorSecondary","colorInherit","colorTransparent","colorError","colorInfo","colorSuccess","colorWarning"]);const Mgr=n=>{const{color:e,position:t,classes:i}=n,r={root:["root",`color${ri(e)}`,`position${ri(t)}`]};return jo(r,Rgr,i)},mPn=(n,e)=>n?`${n.replace(")","")}, ${e})`:e,Ogr=nn(Qf,{name:"MuiAppBar",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[`position${ri(t.position)}`],e[`color${ri(t.color)}`]]}})(Gs(({theme:n})=>({display:"flex",flexDirection:"column",width:"100%",boxSizing:"border-box",flexShrink:0,variants:[{props:{position:"fixed"},style:{position:"fixed",zIndex:(n.vars||n).zIndex.appBar,top:0,left:"auto",right:0,"@media print":{position:"absolute"}}},{props:{position:"absolute"},style:{position:"absolute",zIndex:(n.vars||n).zIndex.appBar,top:0,left:"auto",right:0}},{props:{position:"sticky"},style:{position:"sticky",zIndex:(n.vars||n).zIndex.appBar,top:0,left:"auto",right:0}},{props:{position:"static"},style:{position:"static"}},{props:{position:"relative"},style:{position:"relative"}},{props:{color:"inherit"},style:{"--AppBar-color":"inherit",color:"var(--AppBar-color)"}},{props:{color:"default"},style:{"--AppBar-background":n.vars?n.vars.palette.AppBar.defaultBg:n.palette.grey[100],"--AppBar-color":n.vars?n.vars.palette.text.primary:n.palette.getContrastText(n.palette.grey[100]),...n.applyStyles("dark",{"--AppBar-background":n.vars?n.vars.palette.AppBar.defaultBg:n.palette.grey[900],"--AppBar-color":n.vars?n.vars.palette.text.primary:n.palette.getContrastText(n.palette.grey[900])})}},...Object.entries(n.palette).filter(Vh(["contrastText"])).map(([e])=>({props:{color:e},style:{"--AppBar-background":(n.vars??n).palette[e].main,"--AppBar-color":(n.vars??n).palette[e].contrastText}})),{props:e=>e.enableColorOnDark===!0&&!["inherit","transparent"].includes(e.color),style:{backgroundColor:"var(--AppBar-background)",color:"var(--AppBar-color)"}},{props:e=>e.enableColorOnDark===!1&&!["inherit","transparent"].includes(e.color),style:{backgroundColor:"var(--AppBar-background)",color:"var(--AppBar-color)",...n.applyStyles("dark",{backgroundColor:n.vars?mPn(n.vars.palette.AppBar.darkBg,"var(--AppBar-background)"):null,color:n.vars?mPn(n.vars.palette.AppBar.darkColor,"var(--AppBar-color)"):null})}},{props:{color:"transparent"},style:{"--AppBar-background":"transparent","--AppBar-color":"inherit",backgroundColor:"var(--AppBar-background)",color:"var(--AppBar-color)",...n.applyStyles("dark",{backgroundImage:"none"})}}]}))),Ngr=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiAppBar"}),{className:r,color:o="primary",enableColorOnDark:l=!1,position:c="fixed",...d}=i,h={...i,color:o,position:c,enableColorOnDark:l},p=Mgr(h);return k.jsx(Ogr,{square:!0,component:"header",ownerState:h,elevation:4,className:_i(p.root,r,c==="fixed"&&"mui-fixed"),ref:t,...d})});function Xzt(n){const e=L.useRef({});return L.useEffect(()=>{e.current=n}),e.current}function bPn({array1:n,array2:e,parser:t=i=>i}){return n&&e&&n.length===e.length&&n.every((i,r)=>t(i)===t(e[r]))}function vPn(n){return n.normalize("NFD").replace(/[\u0300-\u036f]/g,"")}function Qzt(n={}){const{ignoreAccents:e=!0,ignoreCase:t=!0,limit:i,matchFrom:r="any",stringify:o,trim:l=!1}=n;return(c,{inputValue:d,getOptionLabel:h})=>{let p=l?d.trim():d;t&&(p=p.toLowerCase()),e&&(p=vPn(p));const m=p?c.filter(b=>{let w=(o||h)(b);return t&&(w=w.toLowerCase()),e&&(w=vPn(w)),r==="start"?w.startsWith(p):w.includes(p)}):c;return typeof i=="number"?m.slice(0,i):m}}const Pgr=Qzt(),wPn=5,Fgr=n=>n.current!==null&&n.current.parentElement?.contains(document.activeElement),jgr=[];function yPn(n,e,t,i){if(e||n==null||i)return"";const r=t(n);return typeof r=="string"?r:""}function Hgr(n){const{unstable_isActiveElementInListbox:e=Fgr,unstable_classNamePrefix:t="Mui",autoComplete:i=!1,autoHighlight:r=!1,autoSelect:o=!1,blurOnSelect:l=!1,clearOnBlur:c=!n.freeSolo,clearOnEscape:d=!1,componentName:h="useAutocomplete",defaultValue:p=n.multiple?jgr:null,disableClearable:m=!1,disableCloseOnSelect:b=!1,disabled:w,disabledItemsFocusable:_=!1,disableListWrap:x=!1,filterOptions:T=Pgr,filterSelectedOptions:I=!1,freeSolo:D=!1,getOptionDisabled:A,getOptionKey:M,getOptionLabel:O=un=>un.label??un,groupBy:F,handleHomeEndKeys:j=!n.freeSolo,id:W,includeInputInList:U=!1,inputValue:Z,isOptionEqualToValue:te=(un,bt)=>un===bt,multiple:G=!1,onChange:ee,onClose:Q,onHighlightChange:ie,onInputChange:se,onOpen:ue,open:ne,openOnFocus:we=!1,options:de,readOnly:ce=!1,renderValue:ye,selectOnFocus:he=!n.freeSolo,value:pe}=n,me=KW(W);let be=O;be=un=>{const bt=O(un);return typeof bt!="string"?String(bt):bt};const xe=L.useRef(!1),Te=L.useRef(!0),qe=L.useRef(null),et=L.useRef(null),[Ge,Me]=L.useState(null),[He,lt]=L.useState(-1),st=r?0:-1,Be=L.useRef(st),ot=L.useRef(yPn(p??pe,G,be)).current,[ct,ze]=E9({controlled:pe,default:p,name:h}),[Ke,$e]=E9({controlled:Z,default:ot,name:h,state:"inputValue"}),[tt,vt]=L.useState(!1),Ft=L.useCallback((un,bt,Cn)=>{if(!(G?ct.length!(I&&(G?ct:[ct]).some(bt=>bt!==null&&te(un,bt)))),{inputValue:Jt&&St?"":Ke,getOptionLabel:be}):[],Gi=Xzt({filteredOptions:dn,value:ct,inputValue:Ke});L.useEffect(()=>{const un=ct!==Gi.value;tt&&!un||D&&!un||Ft(null,ct,"reset")},[ct,Ft,tt,Gi.value,D]);const $i=_t&&dn.length>0&&!ce,Dr=db(un=>{if(un===-1)qe.current.focus();else{const bt=ye?"data-item-index":"data-tag-index";Ge.querySelector(`[${bt}="${un}"]`).focus()}});L.useEffect(()=>{G&&He>ct.length-1&&(lt(-1),Dr(-1))},[ct,G,He,Dr]);function ps(un,bt){if(!et.current||un<0||un>=dn.length)return-1;let Cn=un;for(;;){const Di=et.current.querySelector(`[data-option-index="${Cn}"]`),Ni=_?!1:!Di||Di.disabled||Di.getAttribute("aria-disabled")==="true";if(Di&&Di.hasAttribute("tabindex")&&!Ni)return Cn;if(bt==="next"?Cn=(Cn+1)%dn.length:Cn=(Cn-1+dn.length)%dn.length,Cn===un)return-1}}const rn=db(({event:un,index:bt,reason:Cn})=>{if(Be.current=bt,bt===-1?qe.current.removeAttribute("aria-activedescendant"):qe.current.setAttribute("aria-activedescendant",`${me}-option-${bt}`),ie&&["mouse","keyboard","touch"].includes(Cn)&&ie(un,bt===-1?null:dn[bt],Cn),!et.current)return;const Di=et.current.querySelector(`[role="option"].${t}-focused`);Di&&(Di.classList.remove(`${t}-focused`),Di.classList.remove(`${t}-focusVisible`));let Ni=et.current;if(et.current.getAttribute("role")!=="listbox"&&(Ni=et.current.parentElement.querySelector('[role="listbox"]')),!Ni)return;if(bt===-1){Ni.scrollTop=0;return}const Ds=et.current.querySelector(`[data-option-index="${bt}"]`);if(Ds&&(Ds.classList.add(`${t}-focused`),Cn==="keyboard"&&Ds.classList.add(`${t}-focusVisible`),Ni.scrollHeight>Ni.clientHeight&&Cn!=="mouse"&&Cn!=="touch")){const Es=Ds,$a=Ni.clientHeight+Ni.scrollTop,Lu=Es.offsetTop+Es.offsetHeight;Lu>$a?Ni.scrollTop=Lu-Ni.clientHeight:Es.offsetTop-Es.offsetHeight*(F?1.3:0){if(!fn)return;const Ds=ps((()=>{const Es=dn.length-1;if(bt==="reset")return st;if(bt==="start")return 0;if(bt==="end")return Es;const $a=Be.current+bt;return $a<0?$a===-1&&U?-1:x&&Be.current!==-1||Math.abs(bt)>1?0:Es:$a>Es?$a===Es+1&&U?-1:x||Math.abs(bt)>1?Es:0:$a})(),Cn);if(rn({index:Ds,reason:Di,event:un}),i&&bt!=="reset")if(Ds===-1)qe.current.value=Ke;else{const Es=be(dn[Ds]);qe.current.value=Es,Es.toLowerCase().indexOf(Ke.toLowerCase())===0&&Ke.length>0&&qe.current.setSelectionRange(Ke.length,Es.length)}}),Ei=!bPn({array1:Gi.filteredOptions,array2:dn,parser:be}),gr=()=>{const un=(bt,Cn)=>{const Di=bt?be(bt):"",Ni=Cn?be(Cn):"";return Di===Ni};if(Be.current!==-1&&!bPn({array1:Gi.filteredOptions,array2:dn,parser:be})&&Gi.inputValue===Ke&&(G?ct.length===Gi.value.length&&Gi.value.every((bt,Cn)=>be(ct[Cn])===be(bt)):un(Gi.value,ct))){const bt=Gi.filteredOptions[Be.current];if(bt)return dn.findIndex(Cn=>be(Cn)===be(bt))}return-1},ss=L.useCallback(()=>{if(!fn)return;const un=gr();if(un!==-1){Be.current=un;return}const bt=G?ct[0]:ct;if(dn.length===0||bt==null){xt({diff:"reset"});return}if(et.current){if(bt!=null){const Cn=dn[Be.current];if(G&&Cn&&ct.findIndex(Ni=>te(Cn,Ni))!==-1)return;const Di=dn.findIndex(Ni=>te(Ni,bt));Di===-1?xt({diff:"reset"}):rn({index:Di});return}if(Be.current>=dn.length-1){rn({index:dn.length-1});return}rn({index:Be.current})}},[dn.length,G?!1:ct,xt,rn,fn,Ke,G]),us=db(un=>{zOt(et,un),un&&ss()});L.useEffect(()=>{(Ei||fn&&!b)&&ss()},[ss,Ei,fn,b]);const _r=un=>{_t||(it(!0),Ot(!0),ue&&ue(un))},uo=(un,bt)=>{_t&&(it(!1),Q&&Q(un,bt))},xs=(un,bt,Cn,Di)=>{if(G){if(ct.length===bt.length&&ct.every((Ni,Ds)=>Ni===bt[Ds]))return}else if(ct===bt)return;ee&&ee(un,bt,Cn,Di),ze(bt)},Fs=L.useRef(!1),eo=(un,bt,Cn="selectOption",Di="options")=>{let Ni=Cn,Ds=bt;if(G){Ds=Array.isArray(ct)?ct.slice():[];const Es=Ds.findIndex($a=>te(bt,$a));Es===-1?Ds.push(bt):Di!=="freeSolo"&&(Ds.splice(Es,1),Ni="removeOption")}Ft(un,Ds,Ni),xs(un,Ds,Ni,{option:bt}),!b&&(!un||!un.ctrlKey&&!un.metaKey)&&uo(un,Ni),(l===!0||l==="touch"&&Fs.current||l==="mouse"&&!Fs.current)&&qe.current.blur()};function Ri(un,bt){if(un===-1)return-1;let Cn=un;for(;;){if(bt==="next"&&Cn===ct.length||bt==="previous"&&Cn===-1)return-1;const Di=ye?"data-item-index":"data-tag-index",Ni=Ge.querySelector(`[${Di}="${Cn}"]`);if(!Ni||!Ni.hasAttribute("tabindex")||Ni.disabled||Ni.getAttribute("aria-disabled")==="true")Cn+=bt==="next"?1:-1;else return Cn}}const Ls=(un,bt)=>{if(!G)return;Ke===""&&uo(un,"toggleInput");let Cn=He;He===-1&&bt==="previous"?(Cn=ct.length-1,D&&Ke!==""&&($e(""),se&&se(un,"","reset"))):(Cn+=bt==="next"?1:-1,Cn<0&&(Cn=0),Cn===ct.length&&(Cn=-1)),Cn=Ri(Cn,bt),lt(Cn),Dr(Cn)},Cr=un=>{xe.current=!0,$e(""),se&&se(un,"","clear"),xs(un,G?[]:null,"clear")},Sr=un=>bt=>{if(un.onKeyDown&&un.onKeyDown(bt),!bt.defaultMuiPrevented&&(He!==-1&&!["ArrowLeft","ArrowRight"].includes(bt.key)&&(lt(-1),Dr(-1)),bt.which!==229))switch(bt.key){case"Home":fn&&j&&(bt.preventDefault(),xt({diff:"start",direction:"next",reason:"keyboard",event:bt}));break;case"End":fn&&j&&(bt.preventDefault(),xt({diff:"end",direction:"previous",reason:"keyboard",event:bt}));break;case"PageUp":bt.preventDefault(),xt({diff:-wPn,direction:"previous",reason:"keyboard",event:bt}),_r(bt);break;case"PageDown":bt.preventDefault(),xt({diff:wPn,direction:"next",reason:"keyboard",event:bt}),_r(bt);break;case"ArrowDown":bt.preventDefault(),xt({diff:1,direction:"next",reason:"keyboard",event:bt}),_r(bt);break;case"ArrowUp":bt.preventDefault(),xt({diff:-1,direction:"previous",reason:"keyboard",event:bt}),_r(bt);break;case"ArrowLeft":{const Cn=qe.current;if(!(Cn&&Cn.selectionStart===0&&Cn.selectionEnd===0))return;!G&&ye&&ct!=null?(D&&Ke!==""&&($e(""),se&&se(bt,"","reset")),lt(0),Dr(0)):Ls(bt,"previous");break}case"ArrowRight":!G&&ye?(lt(-1),Dr(-1)):Ls(bt,"next");break;case"Enter":if(Be.current!==-1&&fn){const Cn=dn[Be.current],Di=A?A(Cn):!1;if(bt.preventDefault(),Di)return;eo(bt,Cn,"selectOption"),i&&qe.current.setSelectionRange(qe.current.value.length,qe.current.value.length)}else D&&Ke!==""&&Jt===!1&&(G&&bt.preventDefault(),eo(bt,Ke,"createOption","freeSolo"));break;case"Escape":fn?(bt.preventDefault(),bt.stopPropagation(),uo(bt,"escape")):d&&(Ke!==""||G&&ct.length>0||ye)&&(bt.preventDefault(),bt.stopPropagation(),Cr(bt));break;case"Backspace":if(G&&!ce&&Ke===""&&ct.length>0){const Cn=He===-1?ct.length-1:He,Di=ct.slice();Di.splice(Cn,1),xs(bt,Di,"removeOption",{option:ct[Cn]})}!G&&ye&&!ce&&Ke===""&&xs(bt,null,"removeOption",{option:ct});break;case"Delete":if(G&&!ce&&Ke===""&&ct.length>0&&He!==-1){const Cn=He,Di=ct.slice();Di.splice(Cn,1),xs(bt,Di,"removeOption",{option:ct[Cn]})}!G&&ye&&!ce&&Ke===""&&xs(bt,null,"removeOption",{option:ct});break}},os=un=>{vt(!0),He!==-1&&(lt(-1),Dr(-1)),we&&!xe.current&&_r(un)},Ks=un=>{if(e(et)){qe.current.focus();return}vt(!1),Te.current=!0,xe.current=!1,o&&Be.current!==-1&&fn?eo(un,dn[Be.current],"blur"):o&&D&&Ke!==""?eo(un,Ke,"blur","freeSolo"):c&&Ft(un,ct,"blur"),uo(un,"blur")},jt=un=>{const bt=un.target.value;Ke!==bt&&($e(bt),Ot(!1),se&&se(un,bt,"input")),bt===""?!m&&!G&&!ye&&xs(un,null,"clear"):_r(un)},sn=un=>{const bt=Number(un.currentTarget.getAttribute("data-option-index"));Be.current!==bt&&rn({event:un,index:bt,reason:"mouse"})},zn=un=>{rn({event:un,index:Number(un.currentTarget.getAttribute("data-option-index")),reason:"touch"}),Fs.current=!0},Oi=un=>{const bt=Number(un.currentTarget.getAttribute("data-option-index"));eo(un,dn[bt],"selectOption"),Fs.current=!1},Yi=un=>bt=>{const Cn=ct.slice();Cn.splice(un,1),xs(bt,Cn,"removeOption",{option:ct[un]})},gs=un=>{xs(un,null,"removeOption",{option:ct})},ur=un=>{_t?uo(un,"toggleInput"):_r(un)},vs=un=>{un.currentTarget.contains(un.target)&&un.target.getAttribute("id")!==me&&un.preventDefault()},Ir=un=>{un.currentTarget.contains(un.target)&&(qe.current.focus(),he&&Te.current&&qe.current.selectionEnd-qe.current.selectionStart===0&&qe.current.select(),Te.current=!1)},oo=un=>{!w&&(Ke===""||!_t)&&ur(un)};let Io=D&&Ke.length>0;Io=Io||(G?ct.length>0:ct!==null);let zs=dn;return F&&(zs=dn.reduce((un,bt,Cn)=>{const Di=F(bt);return un.length>0&&un[un.length-1].group===Di?un[un.length-1].options.push(bt):un.push({key:Cn,index:Cn,group:Di,options:[bt]}),un},[])),w&&tt&&Ks(),{getRootProps:(un={})=>({...un,onKeyDown:Sr(un),onMouseDown:vs,onClick:Ir}),getInputLabelProps:()=>({id:`${me}-label`,htmlFor:me}),getInputProps:()=>({id:me,value:Ke,onBlur:Ks,onFocus:os,onChange:jt,onMouseDown:oo,"aria-activedescendant":fn?"":null,"aria-autocomplete":i?"both":"list","aria-controls":$i?`${me}-listbox`:void 0,"aria-expanded":$i,autoComplete:"off",ref:qe,autoCapitalize:"none",spellCheck:"false",role:"combobox",disabled:w}),getClearProps:()=>({tabIndex:-1,type:"button",onClick:Cr}),getItemProps:({index:un=0}={})=>({...G&&{key:un},...ye?{"data-item-index":un}:{"data-tag-index":un},tabIndex:-1,...!ce&&{onDelete:G?Yi(un):gs}}),getPopupIndicatorProps:()=>({tabIndex:-1,type:"button",onClick:ur}),getTagProps:({index:un})=>({key:un,"data-tag-index":un,tabIndex:-1,...!ce&&{onDelete:Yi(un)}}),getListboxProps:()=>({role:"listbox",id:`${me}-listbox`,"aria-labelledby":`${me}-label`,"aria-multiselectable":G||void 0,ref:us,onMouseDown:un=>{un.preventDefault()}}),getOptionProps:({index:un,option:bt})=>{const Cn=(G?ct:[ct]).some(Ni=>Ni!=null&&te(bt,Ni)),Di=A?A(bt):!1;return{key:M?.(bt)??be(bt),tabIndex:-1,role:"option",id:`${me}-option-${un}`,onMouseMove:sn,onClick:Oi,onTouchStart:zn,"data-option-index":un,"aria-disabled":Di,"aria-selected":Cn}},id:me,inputValue:Ke,value:ct,dirty:Io,expanded:fn&&Ge,popupOpen:fn,focused:tt||He!==-1,anchorEl:Ge,setAnchorEl:Me,focusedItem:He,focusedTag:He,groupedOptions:zs}}var Z4="top",I3="bottom",A3="right",X4="left",Jzt="auto",m5e=[Z4,I3,A3,X4],dme="start",h6e="end",Bgr="clippingParents",rui="viewport",Nxe="popper",Wgr="reference",_Pn=m5e.reduce(function(n,e){return n.concat([e+"-"+dme,e+"-"+h6e])},[]),sui=[].concat(m5e,[Jzt]).reduce(function(n,e){return n.concat([e,e+"-"+dme,e+"-"+h6e])},[]),Vgr="beforeRead",$gr="read",zgr="afterRead",Ugr="beforeMain",qgr="main",Ggr="afterMain",Kgr="beforeWrite",Ygr="write",Zgr="afterWrite",Xgr=[Vgr,$gr,zgr,Ugr,qgr,Ggr,Kgr,Ygr,Zgr];function U9(n){return n?(n.nodeName||"").toLowerCase():null}function YL(n){if(n==null)return window;if(n.toString()!=="[object Window]"){var e=n.ownerDocument;return e&&e.defaultView||window}return n}function Jre(n){var e=YL(n).Element;return n instanceof e||n instanceof Element}function w3(n){var e=YL(n).HTMLElement;return n instanceof e||n instanceof HTMLElement}function eUt(n){if(typeof ShadowRoot>"u")return!1;var e=YL(n).ShadowRoot;return n instanceof e||n instanceof ShadowRoot}function Qgr(n){var e=n.state;Object.keys(e.elements).forEach(function(t){var i=e.styles[t]||{},r=e.attributes[t]||{},o=e.elements[t];!w3(o)||!U9(o)||(Object.assign(o.style,i),Object.keys(r).forEach(function(l){var c=r[l];c===!1?o.removeAttribute(l):o.setAttribute(l,c===!0?"":c)}))})}function Jgr(n){var e=n.state,t={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,t.popper),e.styles=t,e.elements.arrow&&Object.assign(e.elements.arrow.style,t.arrow),function(){Object.keys(e.elements).forEach(function(i){var r=e.elements[i],o=e.attributes[i]||{},l=Object.keys(e.styles.hasOwnProperty(i)?e.styles[i]:t[i]),c=l.reduce(function(d,h){return d[h]="",d},{});!w3(r)||!U9(r)||(Object.assign(r.style,c),Object.keys(o).forEach(function(d){r.removeAttribute(d)}))})}}const emr={name:"applyStyles",enabled:!0,phase:"write",fn:Qgr,effect:Jgr,requires:["computeStyles"]};function T9(n){return n.split("-")[0]}var vie=Math.max,sGe=Math.min,hme=Math.round;function YOt(){var n=navigator.userAgentData;return n!=null&&n.brands&&Array.isArray(n.brands)?n.brands.map(function(e){return e.brand+"/"+e.version}).join(" "):navigator.userAgent}function oui(){return!/^((?!chrome|android).)*safari/i.test(YOt())}function fme(n,e,t){e===void 0&&(e=!1),t===void 0&&(t=!1);var i=n.getBoundingClientRect(),r=1,o=1;e&&w3(n)&&(r=n.offsetWidth>0&&hme(i.width)/n.offsetWidth||1,o=n.offsetHeight>0&&hme(i.height)/n.offsetHeight||1);var l=Jre(n)?YL(n):window,c=l.visualViewport,d=!oui()&&t,h=(i.left+(d&&c?c.offsetLeft:0))/r,p=(i.top+(d&&c?c.offsetTop:0))/o,m=i.width/r,b=i.height/o;return{width:m,height:b,top:p,right:h+m,bottom:p+b,left:h,x:h,y:p}}function tUt(n){var e=fme(n),t=n.offsetWidth,i=n.offsetHeight;return Math.abs(e.width-t)<=1&&(t=e.width),Math.abs(e.height-i)<=1&&(i=e.height),{x:n.offsetLeft,y:n.offsetTop,width:t,height:i}}function aui(n,e){var t=e.getRootNode&&e.getRootNode();if(n.contains(e))return!0;if(t&&eUt(t)){var i=e;do{if(i&&n.isSameNode(i))return!0;i=i.parentNode||i.host}while(i)}return!1}function bW(n){return YL(n).getComputedStyle(n)}function tmr(n){return["table","td","th"].indexOf(U9(n))>=0}function FY(n){return((Jre(n)?n.ownerDocument:n.document)||window.document).documentElement}function $et(n){return U9(n)==="html"?n:n.assignedSlot||n.parentNode||(eUt(n)?n.host:null)||FY(n)}function CPn(n){return!w3(n)||bW(n).position==="fixed"?null:n.offsetParent}function nmr(n){var e=/firefox/i.test(YOt()),t=/Trident/i.test(YOt());if(t&&w3(n)){var i=bW(n);if(i.position==="fixed")return null}var r=$et(n);for(eUt(r)&&(r=r.host);w3(r)&&["html","body"].indexOf(U9(r))<0;){var o=bW(r);if(o.transform!=="none"||o.perspective!=="none"||o.contain==="paint"||["transform","perspective"].indexOf(o.willChange)!==-1||e&&o.willChange==="filter"||e&&o.filter&&o.filter!=="none")return r;r=r.parentNode}return null}function b5e(n){for(var e=YL(n),t=CPn(n);t&&tmr(t)&&bW(t).position==="static";)t=CPn(t);return t&&(U9(t)==="html"||U9(t)==="body"&&bW(t).position==="static")?e:t||nmr(n)||e}function nUt(n){return["top","bottom"].indexOf(n)>=0?"x":"y"}function i4e(n,e,t){return vie(n,sGe(e,t))}function imr(n,e,t){var i=i4e(n,e,t);return i>t?t:i}function lui(){return{top:0,right:0,bottom:0,left:0}}function cui(n){return Object.assign({},lui(),n)}function uui(n,e){return e.reduce(function(t,i){return t[i]=n,t},{})}var rmr=function(e,t){return e=typeof e=="function"?e(Object.assign({},t.rects,{placement:t.placement})):e,cui(typeof e!="number"?e:uui(e,m5e))};function smr(n){var e,t=n.state,i=n.name,r=n.options,o=t.elements.arrow,l=t.modifiersData.popperOffsets,c=T9(t.placement),d=nUt(c),h=[X4,A3].indexOf(c)>=0,p=h?"height":"width";if(!(!o||!l)){var m=rmr(r.padding,t),b=tUt(o),w=d==="y"?Z4:X4,_=d==="y"?I3:A3,x=t.rects.reference[p]+t.rects.reference[d]-l[d]-t.rects.popper[p],T=l[d]-t.rects.reference[d],I=b5e(o),D=I?d==="y"?I.clientHeight||0:I.clientWidth||0:0,A=x/2-T/2,M=m[w],O=D-b[p]-m[_],F=D/2-b[p]/2+A,j=i4e(M,F,O),W=d;t.modifiersData[i]=(e={},e[W]=j,e.centerOffset=j-F,e)}}function omr(n){var e=n.state,t=n.options,i=t.element,r=i===void 0?"[data-popper-arrow]":i;r!=null&&(typeof r=="string"&&(r=e.elements.popper.querySelector(r),!r)||aui(e.elements.popper,r)&&(e.elements.arrow=r))}const amr={name:"arrow",enabled:!0,phase:"main",fn:smr,effect:omr,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function pme(n){return n.split("-")[1]}var lmr={top:"auto",right:"auto",bottom:"auto",left:"auto"};function cmr(n,e){var t=n.x,i=n.y,r=e.devicePixelRatio||1;return{x:hme(t*r)/r||0,y:hme(i*r)/r||0}}function SPn(n){var e,t=n.popper,i=n.popperRect,r=n.placement,o=n.variation,l=n.offsets,c=n.position,d=n.gpuAcceleration,h=n.adaptive,p=n.roundOffsets,m=n.isFixed,b=l.x,w=b===void 0?0:b,_=l.y,x=_===void 0?0:_,T=typeof p=="function"?p({x:w,y:x}):{x:w,y:x};w=T.x,x=T.y;var I=l.hasOwnProperty("x"),D=l.hasOwnProperty("y"),A=X4,M=Z4,O=window;if(h){var F=b5e(t),j="clientHeight",W="clientWidth";if(F===YL(t)&&(F=FY(t),bW(F).position!=="static"&&c==="absolute"&&(j="scrollHeight",W="scrollWidth")),F=F,r===Z4||(r===X4||r===A3)&&o===h6e){M=I3;var U=m&&F===O&&O.visualViewport?O.visualViewport.height:F[j];x-=U-i.height,x*=d?1:-1}if(r===X4||(r===Z4||r===I3)&&o===h6e){A=A3;var Z=m&&F===O&&O.visualViewport?O.visualViewport.width:F[W];w-=Z-i.width,w*=d?1:-1}}var te=Object.assign({position:c},h&&lmr),G=p===!0?cmr({x:w,y:x},YL(t)):{x:w,y:x};if(w=G.x,x=G.y,d){var ee;return Object.assign({},te,(ee={},ee[M]=D?"0":"",ee[A]=I?"0":"",ee.transform=(O.devicePixelRatio||1)<=1?"translate("+w+"px, "+x+"px)":"translate3d("+w+"px, "+x+"px, 0)",ee))}return Object.assign({},te,(e={},e[M]=D?x+"px":"",e[A]=I?w+"px":"",e.transform="",e))}function umr(n){var e=n.state,t=n.options,i=t.gpuAcceleration,r=i===void 0?!0:i,o=t.adaptive,l=o===void 0?!0:o,c=t.roundOffsets,d=c===void 0?!0:c,h={placement:T9(e.placement),variation:pme(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:r,isFixed:e.options.strategy==="fixed"};e.modifiersData.popperOffsets!=null&&(e.styles.popper=Object.assign({},e.styles.popper,SPn(Object.assign({},h,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:l,roundOffsets:d})))),e.modifiersData.arrow!=null&&(e.styles.arrow=Object.assign({},e.styles.arrow,SPn(Object.assign({},h,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:d})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})}const dmr={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:umr,data:{}};var MBe={passive:!0};function hmr(n){var e=n.state,t=n.instance,i=n.options,r=i.scroll,o=r===void 0?!0:r,l=i.resize,c=l===void 0?!0:l,d=YL(e.elements.popper),h=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&h.forEach(function(p){p.addEventListener("scroll",t.update,MBe)}),c&&d.addEventListener("resize",t.update,MBe),function(){o&&h.forEach(function(p){p.removeEventListener("scroll",t.update,MBe)}),c&&d.removeEventListener("resize",t.update,MBe)}}const fmr={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:hmr,data:{}};var pmr={left:"right",right:"left",bottom:"top",top:"bottom"};function wUe(n){return n.replace(/left|right|bottom|top/g,function(e){return pmr[e]})}var gmr={start:"end",end:"start"};function xPn(n){return n.replace(/start|end/g,function(e){return gmr[e]})}function iUt(n){var e=YL(n),t=e.pageXOffset,i=e.pageYOffset;return{scrollLeft:t,scrollTop:i}}function rUt(n){return fme(FY(n)).left+iUt(n).scrollLeft}function mmr(n,e){var t=YL(n),i=FY(n),r=t.visualViewport,o=i.clientWidth,l=i.clientHeight,c=0,d=0;if(r){o=r.width,l=r.height;var h=oui();(h||!h&&e==="fixed")&&(c=r.offsetLeft,d=r.offsetTop)}return{width:o,height:l,x:c+rUt(n),y:d}}function bmr(n){var e,t=FY(n),i=iUt(n),r=(e=n.ownerDocument)==null?void 0:e.body,o=vie(t.scrollWidth,t.clientWidth,r?r.scrollWidth:0,r?r.clientWidth:0),l=vie(t.scrollHeight,t.clientHeight,r?r.scrollHeight:0,r?r.clientHeight:0),c=-i.scrollLeft+rUt(n),d=-i.scrollTop;return bW(r||t).direction==="rtl"&&(c+=vie(t.clientWidth,r?r.clientWidth:0)-o),{width:o,height:l,x:c,y:d}}function sUt(n){var e=bW(n),t=e.overflow,i=e.overflowX,r=e.overflowY;return/auto|scroll|overlay|hidden/.test(t+r+i)}function dui(n){return["html","body","#document"].indexOf(U9(n))>=0?n.ownerDocument.body:w3(n)&&sUt(n)?n:dui($et(n))}function r4e(n,e){var t;e===void 0&&(e=[]);var i=dui(n),r=i===((t=n.ownerDocument)==null?void 0:t.body),o=YL(i),l=r?[o].concat(o.visualViewport||[],sUt(i)?i:[]):i,c=e.concat(l);return r?c:c.concat(r4e($et(l)))}function ZOt(n){return Object.assign({},n,{left:n.x,top:n.y,right:n.x+n.width,bottom:n.y+n.height})}function vmr(n,e){var t=fme(n,!1,e==="fixed");return t.top=t.top+n.clientTop,t.left=t.left+n.clientLeft,t.bottom=t.top+n.clientHeight,t.right=t.left+n.clientWidth,t.width=n.clientWidth,t.height=n.clientHeight,t.x=t.left,t.y=t.top,t}function EPn(n,e,t){return e===rui?ZOt(mmr(n,t)):Jre(e)?vmr(e,t):ZOt(bmr(FY(n)))}function wmr(n){var e=r4e($et(n)),t=["absolute","fixed"].indexOf(bW(n).position)>=0,i=t&&w3(n)?b5e(n):n;return Jre(i)?e.filter(function(r){return Jre(r)&&aui(r,i)&&U9(r)!=="body"}):[]}function ymr(n,e,t,i){var r=e==="clippingParents"?wmr(n):[].concat(e),o=[].concat(r,[t]),l=o[0],c=o.reduce(function(d,h){var p=EPn(n,h,i);return d.top=vie(p.top,d.top),d.right=sGe(p.right,d.right),d.bottom=sGe(p.bottom,d.bottom),d.left=vie(p.left,d.left),d},EPn(n,l,i));return c.width=c.right-c.left,c.height=c.bottom-c.top,c.x=c.left,c.y=c.top,c}function hui(n){var e=n.reference,t=n.element,i=n.placement,r=i?T9(i):null,o=i?pme(i):null,l=e.x+e.width/2-t.width/2,c=e.y+e.height/2-t.height/2,d;switch(r){case Z4:d={x:l,y:e.y-t.height};break;case I3:d={x:l,y:e.y+e.height};break;case A3:d={x:e.x+e.width,y:c};break;case X4:d={x:e.x-t.width,y:c};break;default:d={x:e.x,y:e.y}}var h=r?nUt(r):null;if(h!=null){var p=h==="y"?"height":"width";switch(o){case dme:d[h]=d[h]-(e[p]/2-t[p]/2);break;case h6e:d[h]=d[h]+(e[p]/2-t[p]/2);break}}return d}function f6e(n,e){e===void 0&&(e={});var t=e,i=t.placement,r=i===void 0?n.placement:i,o=t.strategy,l=o===void 0?n.strategy:o,c=t.boundary,d=c===void 0?Bgr:c,h=t.rootBoundary,p=h===void 0?rui:h,m=t.elementContext,b=m===void 0?Nxe:m,w=t.altBoundary,_=w===void 0?!1:w,x=t.padding,T=x===void 0?0:x,I=cui(typeof T!="number"?T:uui(T,m5e)),D=b===Nxe?Wgr:Nxe,A=n.rects.popper,M=n.elements[_?D:b],O=ymr(Jre(M)?M:M.contextElement||FY(n.elements.popper),d,p,l),F=fme(n.elements.reference),j=hui({reference:F,element:A,placement:r}),W=ZOt(Object.assign({},A,j)),U=b===Nxe?W:F,Z={top:O.top-U.top+I.top,bottom:U.bottom-O.bottom+I.bottom,left:O.left-U.left+I.left,right:U.right-O.right+I.right},te=n.modifiersData.offset;if(b===Nxe&&te){var G=te[r];Object.keys(Z).forEach(function(ee){var Q=[A3,I3].indexOf(ee)>=0?1:-1,ie=[Z4,I3].indexOf(ee)>=0?"y":"x";Z[ee]+=G[ie]*Q})}return Z}function _mr(n,e){e===void 0&&(e={});var t=e,i=t.placement,r=t.boundary,o=t.rootBoundary,l=t.padding,c=t.flipVariations,d=t.allowedAutoPlacements,h=d===void 0?sui:d,p=pme(i),m=p?c?_Pn:_Pn.filter(function(_){return pme(_)===p}):m5e,b=m.filter(function(_){return h.indexOf(_)>=0});b.length===0&&(b=m);var w=b.reduce(function(_,x){return _[x]=f6e(n,{placement:x,boundary:r,rootBoundary:o,padding:l})[T9(x)],_},{});return Object.keys(w).sort(function(_,x){return w[_]-w[x]})}function Cmr(n){if(T9(n)===Jzt)return[];var e=wUe(n);return[xPn(n),e,xPn(e)]}function Smr(n){var e=n.state,t=n.options,i=n.name;if(!e.modifiersData[i]._skip){for(var r=t.mainAxis,o=r===void 0?!0:r,l=t.altAxis,c=l===void 0?!0:l,d=t.fallbackPlacements,h=t.padding,p=t.boundary,m=t.rootBoundary,b=t.altBoundary,w=t.flipVariations,_=w===void 0?!0:w,x=t.allowedAutoPlacements,T=e.options.placement,I=T9(T),D=I===T,A=d||(D||!_?[wUe(T)]:Cmr(T)),M=[T].concat(A).reduce(function(pe,me){return pe.concat(T9(me)===Jzt?_mr(e,{placement:me,boundary:p,rootBoundary:m,padding:h,flipVariations:_,allowedAutoPlacements:x}):me)},[]),O=e.rects.reference,F=e.rects.popper,j=new Map,W=!0,U=M[0],Z=0;Z=0,ie=Q?"width":"height",se=f6e(e,{placement:te,boundary:p,rootBoundary:m,altBoundary:b,padding:h}),ue=Q?ee?A3:X4:ee?I3:Z4;O[ie]>F[ie]&&(ue=wUe(ue));var ne=wUe(ue),we=[];if(o&&we.push(se[G]<=0),c&&we.push(se[ue]<=0,se[ne]<=0),we.every(function(pe){return pe})){U=te,W=!1;break}j.set(te,we)}if(W)for(var de=_?3:1,ce=function(me){var be=M.find(function(xe){var Te=j.get(xe);if(Te)return Te.slice(0,me).every(function(qe){return qe})});if(be)return U=be,"break"},ye=de;ye>0;ye--){var he=ce(ye);if(he==="break")break}e.placement!==U&&(e.modifiersData[i]._skip=!0,e.placement=U,e.reset=!0)}}const xmr={name:"flip",enabled:!0,phase:"main",fn:Smr,requiresIfExists:["offset"],data:{_skip:!1}};function kPn(n,e,t){return t===void 0&&(t={x:0,y:0}),{top:n.top-e.height-t.y,right:n.right-e.width+t.x,bottom:n.bottom-e.height+t.y,left:n.left-e.width-t.x}}function TPn(n){return[Z4,A3,I3,X4].some(function(e){return n[e]>=0})}function Emr(n){var e=n.state,t=n.name,i=e.rects.reference,r=e.rects.popper,o=e.modifiersData.preventOverflow,l=f6e(e,{elementContext:"reference"}),c=f6e(e,{altBoundary:!0}),d=kPn(l,i),h=kPn(c,r,o),p=TPn(d),m=TPn(h);e.modifiersData[t]={referenceClippingOffsets:d,popperEscapeOffsets:h,isReferenceHidden:p,hasPopperEscaped:m},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":p,"data-popper-escaped":m})}const kmr={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:Emr};function Tmr(n,e,t){var i=T9(n),r=[X4,Z4].indexOf(i)>=0?-1:1,o=typeof t=="function"?t(Object.assign({},e,{placement:n})):t,l=o[0],c=o[1];return l=l||0,c=(c||0)*r,[X4,A3].indexOf(i)>=0?{x:c,y:l}:{x:l,y:c}}function Lmr(n){var e=n.state,t=n.options,i=n.name,r=t.offset,o=r===void 0?[0,0]:r,l=sui.reduce(function(p,m){return p[m]=Tmr(m,e.rects,o),p},{}),c=l[e.placement],d=c.x,h=c.y;e.modifiersData.popperOffsets!=null&&(e.modifiersData.popperOffsets.x+=d,e.modifiersData.popperOffsets.y+=h),e.modifiersData[i]=l}const Dmr={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:Lmr};function Imr(n){var e=n.state,t=n.name;e.modifiersData[t]=hui({reference:e.rects.reference,element:e.rects.popper,placement:e.placement})}const Amr={name:"popperOffsets",enabled:!0,phase:"read",fn:Imr,data:{}};function Rmr(n){return n==="x"?"y":"x"}function Mmr(n){var e=n.state,t=n.options,i=n.name,r=t.mainAxis,o=r===void 0?!0:r,l=t.altAxis,c=l===void 0?!1:l,d=t.boundary,h=t.rootBoundary,p=t.altBoundary,m=t.padding,b=t.tether,w=b===void 0?!0:b,_=t.tetherOffset,x=_===void 0?0:_,T=f6e(e,{boundary:d,rootBoundary:h,padding:m,altBoundary:p}),I=T9(e.placement),D=pme(e.placement),A=!D,M=nUt(I),O=Rmr(M),F=e.modifiersData.popperOffsets,j=e.rects.reference,W=e.rects.popper,U=typeof x=="function"?x(Object.assign({},e.rects,{placement:e.placement})):x,Z=typeof U=="number"?{mainAxis:U,altAxis:U}:Object.assign({mainAxis:0,altAxis:0},U),te=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,G={x:0,y:0};if(F){if(o){var ee,Q=M==="y"?Z4:X4,ie=M==="y"?I3:A3,se=M==="y"?"height":"width",ue=F[M],ne=ue+T[Q],we=ue-T[ie],de=w?-W[se]/2:0,ce=D===dme?j[se]:W[se],ye=D===dme?-W[se]:-j[se],he=e.elements.arrow,pe=w&&he?tUt(he):{width:0,height:0},me=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:lui(),be=me[Q],xe=me[ie],Te=i4e(0,j[se],pe[se]),qe=A?j[se]/2-de-Te-be-Z.mainAxis:ce-Te-be-Z.mainAxis,et=A?-j[se]/2+de+Te+xe+Z.mainAxis:ye+Te+xe+Z.mainAxis,Ge=e.elements.arrow&&b5e(e.elements.arrow),Me=Ge?M==="y"?Ge.clientTop||0:Ge.clientLeft||0:0,He=(ee=te?.[M])!=null?ee:0,lt=ue+qe-He-Me,st=ue+et-He,Be=i4e(w?sGe(ne,lt):ne,ue,w?vie(we,st):we);F[M]=Be,G[M]=Be-ue}if(c){var ot,ct=M==="x"?Z4:X4,ze=M==="x"?I3:A3,Ke=F[O],$e=O==="y"?"height":"width",tt=Ke+T[ct],vt=Ke-T[ze],Ft=[Z4,X4].indexOf(I)!==-1,_t=(ot=te?.[O])!=null?ot:0,it=Ft?tt:Ke-j[$e]-W[$e]-_t+Z.altAxis,St=Ft?Ke+j[$e]+W[$e]-_t-Z.altAxis:vt,Ot=w&&Ft?imr(it,Ke,St):i4e(w?it:tt,Ke,w?St:vt);F[O]=Ot,G[O]=Ot-Ke}e.modifiersData[i]=G}}const Omr={name:"preventOverflow",enabled:!0,phase:"main",fn:Mmr,requiresIfExists:["offset"]};function Nmr(n){return{scrollLeft:n.scrollLeft,scrollTop:n.scrollTop}}function Pmr(n){return n===YL(n)||!w3(n)?iUt(n):Nmr(n)}function Fmr(n){var e=n.getBoundingClientRect(),t=hme(e.width)/n.offsetWidth||1,i=hme(e.height)/n.offsetHeight||1;return t!==1||i!==1}function jmr(n,e,t){t===void 0&&(t=!1);var i=w3(e),r=w3(e)&&Fmr(e),o=FY(e),l=fme(n,r,t),c={scrollLeft:0,scrollTop:0},d={x:0,y:0};return(i||!i&&!t)&&((U9(e)!=="body"||sUt(o))&&(c=Pmr(e)),w3(e)?(d=fme(e,!0),d.x+=e.clientLeft,d.y+=e.clientTop):o&&(d.x=rUt(o))),{x:l.left+c.scrollLeft-d.x,y:l.top+c.scrollTop-d.y,width:l.width,height:l.height}}function Hmr(n){var e=new Map,t=new Set,i=[];n.forEach(function(o){e.set(o.name,o)});function r(o){t.add(o.name);var l=[].concat(o.requires||[],o.requiresIfExists||[]);l.forEach(function(c){if(!t.has(c)){var d=e.get(c);d&&r(d)}}),i.push(o)}return n.forEach(function(o){t.has(o.name)||r(o)}),i}function Bmr(n){var e=Hmr(n);return Xgr.reduce(function(t,i){return t.concat(e.filter(function(r){return r.phase===i}))},[])}function Wmr(n){var e;return function(){return e||(e=new Promise(function(t){Promise.resolve().then(function(){e=void 0,t(n())})})),e}}function Vmr(n){var e=n.reduce(function(t,i){var r=t[i.name];return t[i.name]=r?Object.assign({},r,i,{options:Object.assign({},r.options,i.options),data:Object.assign({},r.data,i.data)}):i,t},{});return Object.keys(e).map(function(t){return e[t]})}var LPn={placement:"bottom",modifiers:[],strategy:"absolute"};function DPn(){for(var n=arguments.length,e=new Array(n),t=0;t=19?n?.props?.ref||null:n?.ref||null}function qmr(n){return typeof n=="function"?n():n}const fui=L.forwardRef(function(e,t){const{children:i,container:r,disablePortal:o=!1}=e,[l,c]=L.useState(null),d=xm(L.isValidElement(i)?jY(i):null,t);if(IS(()=>{o||c(qmr(r)||document.body)},[r,o]),IS(()=>{if(l&&!o)return zOt(t,l),()=>{zOt(t,null)}},[t,l,o]),o){if(L.isValidElement(i)){const h={ref:d};return L.cloneElement(i,h)}return i}return l&&KR.createPortal(i,l)});function Gmr(n){return Po("MuiPopper",n)}Fo("MuiPopper",["root"]);function Kmr(n,e){if(e==="ltr")return n;switch(n){case"bottom-end":return"bottom-start";case"bottom-start":return"bottom-end";case"top-end":return"top-start";case"top-start":return"top-end";default:return n}}function XOt(n){return typeof n=="function"?n():n}function Ymr(n){return n.nodeType!==void 0}const Zmr=n=>{const{classes:e}=n;return jo({root:["root"]},Gmr,e)},Xmr={},Qmr=L.forwardRef(function(e,t){const{anchorEl:i,children:r,direction:o,disablePortal:l,modifiers:c,open:d,placement:h,popperOptions:p,popperRef:m,slotProps:b={},slots:w={},TransitionProps:_,ownerState:x,...T}=e,I=L.useRef(null),D=xm(I,t),A=L.useRef(null),M=xm(A,m),O=L.useRef(M);IS(()=>{O.current=M},[M]),L.useImperativeHandle(m,()=>A.current,[]);const F=Kmr(h,o),[j,W]=L.useState(F),[U,Z]=L.useState(XOt(i));L.useEffect(()=>{A.current&&A.current.forceUpdate()}),L.useEffect(()=>{i&&Z(XOt(i))},[i]),IS(()=>{if(!U||!d)return;const ie=ne=>{W(ne.placement)};let se=[{name:"preventOverflow",options:{altBoundary:l}},{name:"flip",options:{altBoundary:l}},{name:"onUpdate",enabled:!0,phase:"afterWrite",fn:({state:ne})=>{ie(ne)}}];c!=null&&(se=se.concat(c)),p&&p.modifiers!=null&&(se=se.concat(p.modifiers));const ue=Umr(U,I.current,{placement:F,...p,modifiers:se});return O.current(ue),()=>{ue.destroy(),O.current(null)}},[U,l,c,d,p,F]);const te={placement:j};_!==null&&(te.TransitionProps=_);const G=Zmr(e),ee=w.root??"div",Q=dE({elementType:ee,externalSlotProps:b.root,externalForwardedProps:T,additionalProps:{role:"tooltip",ref:D},ownerState:e,className:G.root});return k.jsx(ee,{...Q,children:typeof r=="function"?r(te):r})}),Jmr=L.forwardRef(function(e,t){const{anchorEl:i,children:r,container:o,direction:l="ltr",disablePortal:c=!1,keepMounted:d=!1,modifiers:h,open:p,placement:m="bottom",popperOptions:b=Xmr,popperRef:w,style:_,transition:x=!1,slotProps:T={},slots:I={},...D}=e,[A,M]=L.useState(!0),O=()=>{M(!1)},F=()=>{M(!0)};if(!d&&!p&&(!x||A))return null;let j;if(o)j=o;else if(i){const Z=XOt(i);j=Z&&Ymr(Z)?fv(Z).body:fv(null).body}const W=!p&&d&&(!x||A)?"none":void 0,U=x?{in:p,onEnter:O,onExited:F}:void 0;return k.jsx(fui,{disablePortal:c,container:j,children:k.jsx(Qmr,{anchorEl:i,direction:l,disablePortal:c,modifiers:h,ref:t,open:x?!A:p,placement:m,popperOptions:b,popperRef:w,slotProps:T,slots:I,...D,style:{position:"fixed",top:0,left:0,display:W,..._},TransitionProps:U,children:r})})}),e1r=nn(Jmr,{name:"MuiPopper",slot:"Root"})({}),a8=L.forwardRef(function(e,t){const i=OY(),r=Vo({props:e,name:"MuiPopper"}),{anchorEl:o,component:l,components:c,componentsProps:d,container:h,disablePortal:p,keepMounted:m,modifiers:b,open:w,placement:_,popperOptions:x,popperRef:T,transition:I,slots:D,slotProps:A,...M}=r,O=D?.root??c?.Root,F={anchorEl:o,container:h,disablePortal:p,keepMounted:m,modifiers:b,open:w,placement:_,popperOptions:x,popperRef:T,transition:I,...M};return k.jsx(e1r,{as:l,direction:i?"rtl":"ltr",slots:{root:O},slotProps:A??d,...F,ref:t})});function t1r(n){return Po("MuiListSubheader",n)}Fo("MuiListSubheader",["root","colorPrimary","colorInherit","gutters","inset","sticky"]);const n1r=n=>{const{classes:e,color:t,disableGutters:i,inset:r,disableSticky:o}=n,l={root:["root",t!=="default"&&`color${ri(t)}`,!i&&"gutters",r&&"inset",!o&&"sticky"]};return jo(l,t1r,e)},i1r=nn("li",{name:"MuiListSubheader",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.color!=="default"&&e[`color${ri(t.color)}`],!t.disableGutters&&e.gutters,t.inset&&e.inset,!t.disableSticky&&e.sticky]}})(Gs(({theme:n})=>({boxSizing:"border-box",lineHeight:"48px",listStyle:"none",color:(n.vars||n).palette.text.secondary,fontFamily:n.typography.fontFamily,fontWeight:n.typography.fontWeightMedium,fontSize:n.typography.pxToRem(14),variants:[{props:{color:"primary"},style:{color:(n.vars||n).palette.primary.main}},{props:{color:"inherit"},style:{color:"inherit"}},{props:({ownerState:e})=>!e.disableGutters,style:{paddingLeft:16,paddingRight:16}},{props:({ownerState:e})=>e.inset,style:{paddingLeft:72}},{props:({ownerState:e})=>!e.disableSticky,style:{position:"sticky",top:0,zIndex:1,backgroundColor:(n.vars||n).palette.background.paper}}]}))),QOt=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiListSubheader"}),{className:r,color:o="default",component:l="li",disableGutters:c=!1,disableSticky:d=!1,inset:h=!1,...p}=i,m={...i,color:o,component:l,disableGutters:c,disableSticky:d,inset:h},b=n1r(m);return k.jsx(i1r,{as:l,className:_i(b.root,r),ref:t,ownerState:m,...p})});QOt&&(QOt.muiSkipListHighlight=!0);const r1r=Ya(k.jsx("path",{d:"M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"}));function s1r(n){return Po("MuiChip",n)}const jd=Fo("MuiChip",["root","sizeSmall","sizeMedium","colorDefault","colorError","colorInfo","colorPrimary","colorSecondary","colorSuccess","colorWarning","disabled","clickable","clickableColorPrimary","clickableColorSecondary","deletable","deletableColorPrimary","deletableColorSecondary","outlined","filled","outlinedPrimary","outlinedSecondary","filledPrimary","filledSecondary","avatar","avatarSmall","avatarMedium","avatarColorPrimary","avatarColorSecondary","icon","iconSmall","iconMedium","iconColorPrimary","iconColorSecondary","label","labelSmall","labelMedium","deleteIcon","deleteIconSmall","deleteIconMedium","deleteIconColorPrimary","deleteIconColorSecondary","deleteIconOutlinedColorPrimary","deleteIconOutlinedColorSecondary","deleteIconFilledColorPrimary","deleteIconFilledColorSecondary","focusVisible"]),o1r=n=>{const{classes:e,disabled:t,size:i,color:r,iconColor:o,onDelete:l,clickable:c,variant:d}=n,h={root:["root",d,t&&"disabled",`size${ri(i)}`,`color${ri(r)}`,c&&"clickable",c&&`clickableColor${ri(r)}`,l&&"deletable",l&&`deletableColor${ri(r)}`,`${d}${ri(r)}`],label:["label",`label${ri(i)}`],avatar:["avatar",`avatar${ri(i)}`,`avatarColor${ri(r)}`],icon:["icon",`icon${ri(i)}`,`iconColor${ri(o)}`],deleteIcon:["deleteIcon",`deleteIcon${ri(i)}`,`deleteIconColor${ri(r)}`,`deleteIcon${ri(d)}Color${ri(r)}`]};return jo(h,s1r,e)},a1r=nn("div",{name:"MuiChip",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n,{color:i,iconColor:r,clickable:o,onDelete:l,size:c,variant:d}=t;return[{[`& .${jd.avatar}`]:e.avatar},{[`& .${jd.avatar}`]:e[`avatar${ri(c)}`]},{[`& .${jd.avatar}`]:e[`avatarColor${ri(i)}`]},{[`& .${jd.icon}`]:e.icon},{[`& .${jd.icon}`]:e[`icon${ri(c)}`]},{[`& .${jd.icon}`]:e[`iconColor${ri(r)}`]},{[`& .${jd.deleteIcon}`]:e.deleteIcon},{[`& .${jd.deleteIcon}`]:e[`deleteIcon${ri(c)}`]},{[`& .${jd.deleteIcon}`]:e[`deleteIconColor${ri(i)}`]},{[`& .${jd.deleteIcon}`]:e[`deleteIcon${ri(d)}Color${ri(i)}`]},e.root,e[`size${ri(c)}`],e[`color${ri(i)}`],o&&e.clickable,o&&i!=="default"&&e[`clickableColor${ri(i)}`],l&&e.deletable,l&&i!=="default"&&e[`deletableColor${ri(i)}`],e[d],e[`${d}${ri(i)}`]]}})(Gs(({theme:n})=>{const e=n.palette.mode==="light"?n.palette.grey[700]:n.palette.grey[300];return{maxWidth:"100%",fontFamily:n.typography.fontFamily,fontSize:n.typography.pxToRem(13),display:"inline-flex",alignItems:"center",justifyContent:"center",height:32,lineHeight:1.5,color:(n.vars||n).palette.text.primary,backgroundColor:(n.vars||n).palette.action.selected,borderRadius:32/2,whiteSpace:"nowrap",transition:n.transitions.create(["background-color","box-shadow"]),cursor:"unset",outline:0,textDecoration:"none",border:0,padding:0,verticalAlign:"middle",boxSizing:"border-box",[`&.${jd.disabled}`]:{opacity:(n.vars||n).palette.action.disabledOpacity,pointerEvents:"none"},[`& .${jd.avatar}`]:{marginLeft:5,marginRight:-6,width:24,height:24,color:n.vars?n.vars.palette.Chip.defaultAvatarColor:e,fontSize:n.typography.pxToRem(12)},[`& .${jd.avatarColorPrimary}`]:{color:(n.vars||n).palette.primary.contrastText,backgroundColor:(n.vars||n).palette.primary.dark},[`& .${jd.avatarColorSecondary}`]:{color:(n.vars||n).palette.secondary.contrastText,backgroundColor:(n.vars||n).palette.secondary.dark},[`& .${jd.avatarSmall}`]:{marginLeft:4,marginRight:-4,width:18,height:18,fontSize:n.typography.pxToRem(10)},[`& .${jd.icon}`]:{marginLeft:5,marginRight:-6},[`& .${jd.deleteIcon}`]:{WebkitTapHighlightColor:"transparent",color:n.alpha((n.vars||n).palette.text.primary,.26),fontSize:22,cursor:"pointer",margin:"0 5px 0 -6px","&:hover":{color:n.alpha((n.vars||n).palette.text.primary,.4)}},variants:[{props:{size:"small"},style:{height:24,[`& .${jd.icon}`]:{fontSize:18,marginLeft:4,marginRight:-4},[`& .${jd.deleteIcon}`]:{fontSize:16,marginRight:4,marginLeft:-4}}},...Object.entries(n.palette).filter(Vh(["contrastText"])).map(([t])=>({props:{color:t},style:{backgroundColor:(n.vars||n).palette[t].main,color:(n.vars||n).palette[t].contrastText,[`& .${jd.deleteIcon}`]:{color:n.alpha((n.vars||n).palette[t].contrastText,.7),"&:hover, &:active":{color:(n.vars||n).palette[t].contrastText}}}})),{props:t=>t.iconColor===t.color,style:{[`& .${jd.icon}`]:{color:n.vars?n.vars.palette.Chip.defaultIconColor:e}}},{props:t=>t.iconColor===t.color&&t.color!=="default",style:{[`& .${jd.icon}`]:{color:"inherit"}}},{props:{onDelete:!0},style:{[`&.${jd.focusVisible}`]:{backgroundColor:n.alpha((n.vars||n).palette.action.selected,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.focusOpacity}`)}}},...Object.entries(n.palette).filter(Vh(["dark"])).map(([t])=>({props:{color:t,onDelete:!0},style:{[`&.${jd.focusVisible}`]:{background:(n.vars||n).palette[t].dark}}})),{props:{clickable:!0},style:{userSelect:"none",WebkitTapHighlightColor:"transparent",cursor:"pointer","&:hover":{backgroundColor:n.alpha((n.vars||n).palette.action.selected,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.hoverOpacity}`)},[`&.${jd.focusVisible}`]:{backgroundColor:n.alpha((n.vars||n).palette.action.selected,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.focusOpacity}`)},"&:active":{boxShadow:(n.vars||n).shadows[1]}}},...Object.entries(n.palette).filter(Vh(["dark"])).map(([t])=>({props:{color:t,clickable:!0},style:{[`&:hover, &.${jd.focusVisible}`]:{backgroundColor:(n.vars||n).palette[t].dark}}})),{props:{variant:"outlined"},style:{backgroundColor:"transparent",border:n.vars?`1px solid ${n.vars.palette.Chip.defaultBorder}`:`1px solid ${n.palette.mode==="light"?n.palette.grey[400]:n.palette.grey[700]}`,[`&.${jd.clickable}:hover`]:{backgroundColor:(n.vars||n).palette.action.hover},[`&.${jd.focusVisible}`]:{backgroundColor:(n.vars||n).palette.action.focus},[`& .${jd.avatar}`]:{marginLeft:4},[`& .${jd.avatarSmall}`]:{marginLeft:2},[`& .${jd.icon}`]:{marginLeft:4},[`& .${jd.iconSmall}`]:{marginLeft:2},[`& .${jd.deleteIcon}`]:{marginRight:5},[`& .${jd.deleteIconSmall}`]:{marginRight:3}}},...Object.entries(n.palette).filter(Vh()).map(([t])=>({props:{variant:"outlined",color:t},style:{color:(n.vars||n).palette[t].main,border:`1px solid ${n.alpha((n.vars||n).palette[t].main,.7)}`,[`&.${jd.clickable}:hover`]:{backgroundColor:n.alpha((n.vars||n).palette[t].main,(n.vars||n).palette.action.hoverOpacity)},[`&.${jd.focusVisible}`]:{backgroundColor:n.alpha((n.vars||n).palette[t].main,(n.vars||n).palette.action.focusOpacity)},[`& .${jd.deleteIcon}`]:{color:n.alpha((n.vars||n).palette[t].main,.7),"&:hover, &:active":{color:(n.vars||n).palette[t].main}}}}))]}})),l1r=nn("span",{name:"MuiChip",slot:"Label",overridesResolver:(n,e)=>{const{ownerState:t}=n,{size:i}=t;return[e.label,e[`label${ri(i)}`]]}})({overflow:"hidden",textOverflow:"ellipsis",paddingLeft:12,paddingRight:12,whiteSpace:"nowrap",variants:[{props:{variant:"outlined"},style:{paddingLeft:11,paddingRight:11}},{props:{size:"small"},style:{paddingLeft:8,paddingRight:8}},{props:{size:"small",variant:"outlined"},style:{paddingLeft:7,paddingRight:7}}]});function IPn(n){return n.key==="Backspace"||n.key==="Delete"}const j_=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiChip"}),{avatar:r,className:o,clickable:l,color:c="default",component:d,deleteIcon:h,disabled:p=!1,icon:m,label:b,onClick:w,onDelete:_,onKeyDown:x,onKeyUp:T,size:I="medium",variant:D="filled",tabIndex:A,skipFocusWhenDisabled:M=!1,slots:O={},slotProps:F={},...j}=i,W=L.useRef(null),U=xm(W,t),Z=be=>{be.stopPropagation(),_(be)},te=be=>{be.currentTarget===be.target&&IPn(be)&&be.preventDefault(),x&&x(be)},G=be=>{be.currentTarget===be.target&&_&&IPn(be)&&_(be),T&&T(be)},ee=l!==!1&&w?!0:l,Q=ee||_?D3:d||"div",ie={...i,component:Q,disabled:p,size:I,color:c,iconColor:L.isValidElement(m)&&m.props.color||c,onDelete:!!_,clickable:ee,variant:D},se=o1r(ie),ue=Q===D3?{component:d||"div",focusVisibleClassName:se.focusVisible,..._&&{disableRipple:!0}}:{};let ne=null;_&&(ne=h&&L.isValidElement(h)?L.cloneElement(h,{className:_i(h.props.className,se.deleteIcon),onClick:Z}):k.jsx(r1r,{className:se.deleteIcon,onClick:Z}));let we=null;r&&L.isValidElement(r)&&(we=L.cloneElement(r,{className:_i(se.avatar,r.props.className)}));let de=null;m&&L.isValidElement(m)&&(de=L.cloneElement(m,{className:_i(se.icon,m.props.className)}));const ce={slots:O,slotProps:F},[ye,he]=_o("root",{elementType:a1r,externalForwardedProps:{...ce,...j},ownerState:ie,shouldForwardComponentProp:!0,ref:U,className:_i(se.root,o),additionalProps:{disabled:ee&&p?!0:void 0,tabIndex:M&&p?-1:A,...ue},getSlotProps:be=>({...be,onClick:xe=>{be.onClick?.(xe),w?.(xe)},onKeyDown:xe=>{be.onKeyDown?.(xe),te(xe)},onKeyUp:xe=>{be.onKeyUp?.(xe),G(xe)}})}),[pe,me]=_o("label",{elementType:l1r,externalForwardedProps:ce,ownerState:ie,className:se.label});return k.jsxs(ye,{as:Q,...he,children:[we||de,k.jsx(pe,{...me,children:b}),ne]})});function OBe(n){return parseInt(n,10)||0}const c1r={shadow:{visibility:"hidden",position:"absolute",overflow:"hidden",height:0,top:0,left:0,transform:"translateZ(0)"}};function u1r(n){for(const e in n)return!1;return!0}function APn(n){return u1r(n)||n.outerHeightStyle===0&&!n.overflowing}const d1r=L.forwardRef(function(e,t){const{onChange:i,maxRows:r,minRows:o=1,style:l,value:c,...d}=e,{current:h}=L.useRef(c!=null),p=L.useRef(null),m=xm(t,p),b=L.useRef(null),w=L.useRef(null),_=L.useCallback(()=>{const A=p.current,M=w.current;if(!A||!M)return;const F=KL(A).getComputedStyle(A);if(F.width==="0px")return{outerHeightStyle:0,overflowing:!1};M.style.width=F.width,M.value=A.value||e.placeholder||"x",M.value.slice(-1)===` +`&&(M.value+=" ");const j=F.boxSizing,W=OBe(F.paddingBottom)+OBe(F.paddingTop),U=OBe(F.borderBottomWidth)+OBe(F.borderTopWidth),Z=M.scrollHeight;M.value="x";const te=M.scrollHeight;let G=Z;o&&(G=Math.max(Number(o)*te,G)),r&&(G=Math.min(Number(r)*te,G)),G=Math.max(G,te);const ee=G+(j==="border-box"?W+U:0),Q=Math.abs(G-Z)<=1;return{outerHeightStyle:ee,overflowing:Q}},[r,o,e.placeholder]),x=db(()=>{const A=p.current,M=_();if(!A||!M||APn(M))return!1;const O=M.outerHeightStyle;return b.current!=null&&b.current!==O}),T=L.useCallback(()=>{const A=p.current,M=_();if(!A||!M||APn(M))return;const O=M.outerHeightStyle;b.current!==O&&(b.current=O,A.style.height=`${O}px`),A.style.overflow=M.overflowing?"hidden":""},[_]),I=L.useRef(-1);IS(()=>{const A=p5e(T),M=p?.current;if(!M)return;const O=KL(M);O.addEventListener("resize",A);let F;return typeof ResizeObserver<"u"&&(F=new ResizeObserver(()=>{x()&&(F.unobserve(M),cancelAnimationFrame(I.current),T(),I.current=requestAnimationFrame(()=>{F.observe(M)}))}),F.observe(M)),()=>{A.clear(),cancelAnimationFrame(I.current),O.removeEventListener("resize",A),F&&F.disconnect()}},[_,T,x]),IS(()=>{T()});const D=A=>{h||T();const M=A.target,O=M.value.length,F=M.value.endsWith(` +`),j=M.selectionStart===O;F&&j&&M.setSelectionRange(O,O),i&&i(A)};return k.jsxs(L.Fragment,{children:[k.jsx("textarea",{value:c,onChange:D,ref:m,rows:o,style:l,...d}),k.jsx("textarea",{"aria-hidden":!0,className:e.className,readOnly:!0,ref:w,tabIndex:-1,style:{...c1r.shadow,...l,paddingTop:0,paddingBottom:0}})]})});function HY({props:n,states:e,muiFormControl:t}){return e.reduce((i,r)=>(i[r]=n[r],t&&typeof n[r]>"u"&&(i[r]=t[r]),i),{})}const zet=L.createContext(void 0);function DM(){return L.useContext(zet)}function RPn(n){return n!=null&&!(Array.isArray(n)&&n.length===0)}function oGe(n,e=!1){return n&&(RPn(n.value)&&n.value!==""||e&&RPn(n.defaultValue)&&n.defaultValue!=="")}function h1r(n){return n.startAdornment}function f1r(n){return Po("MuiInputBase",n)}const mL=Fo("MuiInputBase",["root","formControl","focused","disabled","adornedStart","adornedEnd","error","sizeSmall","multiline","colorSecondary","fullWidth","hiddenLabel","readOnly","input","inputSizeSmall","inputMultiline","inputTypeSearch","inputAdornedStart","inputAdornedEnd","inputHiddenLabel"]);var MPn;const Uet=(n,e)=>{const{ownerState:t}=n;return[e.root,t.formControl&&e.formControl,t.startAdornment&&e.adornedStart,t.endAdornment&&e.adornedEnd,t.error&&e.error,t.size==="small"&&e.sizeSmall,t.multiline&&e.multiline,t.color&&e[`color${ri(t.color)}`],t.fullWidth&&e.fullWidth,t.hiddenLabel&&e.hiddenLabel]},qet=(n,e)=>{const{ownerState:t}=n;return[e.input,t.size==="small"&&e.inputSizeSmall,t.multiline&&e.inputMultiline,t.type==="search"&&e.inputTypeSearch,t.startAdornment&&e.inputAdornedStart,t.endAdornment&&e.inputAdornedEnd,t.hiddenLabel&&e.inputHiddenLabel]},p1r=n=>{const{classes:e,color:t,disabled:i,error:r,endAdornment:o,focused:l,formControl:c,fullWidth:d,hiddenLabel:h,multiline:p,readOnly:m,size:b,startAdornment:w,type:_}=n,x={root:["root",`color${ri(t)}`,i&&"disabled",r&&"error",d&&"fullWidth",l&&"focused",c&&"formControl",b&&b!=="medium"&&`size${ri(b)}`,p&&"multiline",w&&"adornedStart",o&&"adornedEnd",h&&"hiddenLabel",m&&"readOnly"],input:["input",i&&"disabled",_==="search"&&"inputTypeSearch",p&&"inputMultiline",b==="small"&&"inputSizeSmall",h&&"inputHiddenLabel",w&&"inputAdornedStart",o&&"inputAdornedEnd",m&&"readOnly"]};return jo(x,f1r,e)},Get=nn("div",{name:"MuiInputBase",slot:"Root",overridesResolver:Uet})(Gs(({theme:n})=>({...n.typography.body1,color:(n.vars||n).palette.text.primary,lineHeight:"1.4375em",boxSizing:"border-box",position:"relative",cursor:"text",display:"inline-flex",alignItems:"center",[`&.${mL.disabled}`]:{color:(n.vars||n).palette.text.disabled,cursor:"default"},variants:[{props:({ownerState:e})=>e.multiline,style:{padding:"4px 0 5px"}},{props:({ownerState:e,size:t})=>e.multiline&&t==="small",style:{paddingTop:1}},{props:({ownerState:e})=>e.fullWidth,style:{width:"100%"}}]}))),Ket=nn("input",{name:"MuiInputBase",slot:"Input",overridesResolver:qet})(Gs(({theme:n})=>{const e=n.palette.mode==="light",t={color:"currentColor",...n.vars?{opacity:n.vars.opacity.inputPlaceholder}:{opacity:e?.42:.5},transition:n.transitions.create("opacity",{duration:n.transitions.duration.shorter})},i={opacity:"0 !important"},r=n.vars?{opacity:n.vars.opacity.inputPlaceholder}:{opacity:e?.42:.5};return{font:"inherit",letterSpacing:"inherit",color:"currentColor",padding:"4px 0 5px",border:0,boxSizing:"content-box",background:"none",height:"1.4375em",margin:0,WebkitTapHighlightColor:"transparent",display:"block",minWidth:0,width:"100%","&::-webkit-input-placeholder":t,"&::-moz-placeholder":t,"&::-ms-input-placeholder":t,"&:focus":{outline:0},"&:invalid":{boxShadow:"none"},"&::-webkit-search-decoration":{WebkitAppearance:"none"},[`label[data-shrink=false] + .${mL.formControl} &`]:{"&::-webkit-input-placeholder":i,"&::-moz-placeholder":i,"&::-ms-input-placeholder":i,"&:focus::-webkit-input-placeholder":r,"&:focus::-moz-placeholder":r,"&:focus::-ms-input-placeholder":r},[`&.${mL.disabled}`]:{opacity:1,WebkitTextFillColor:(n.vars||n).palette.text.disabled},variants:[{props:({ownerState:o})=>!o.disableInjectingGlobalStyles,style:{animationName:"mui-auto-fill-cancel",animationDuration:"10ms","&:-webkit-autofill":{animationDuration:"5000s",animationName:"mui-auto-fill"}}},{props:{size:"small"},style:{paddingTop:1}},{props:({ownerState:o})=>o.multiline,style:{height:"auto",resize:"none",padding:0,paddingTop:0}},{props:{type:"search"},style:{MozAppearance:"textfield"}}]}})),OPn=Nzt({"@keyframes mui-auto-fill":{from:{display:"block"}},"@keyframes mui-auto-fill-cancel":{from:{display:"block"}}}),U1e=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiInputBase"}),{"aria-describedby":r,autoComplete:o,autoFocus:l,className:c,color:d,components:h={},componentsProps:p={},defaultValue:m,disabled:b,disableInjectingGlobalStyles:w,endAdornment:_,error:x,fullWidth:T=!1,id:I,inputComponent:D="input",inputProps:A={},inputRef:M,margin:O,maxRows:F,minRows:j,multiline:W=!1,name:U,onBlur:Z,onChange:te,onClick:G,onFocus:ee,onKeyDown:Q,onKeyUp:ie,placeholder:se,readOnly:ue,renderSuffix:ne,rows:we,size:de,slotProps:ce={},slots:ye={},startAdornment:he,type:pe="text",value:me,...be}=i,xe=A.value!=null?A.value:me,{current:Te}=L.useRef(xe!=null),qe=L.useRef(),et=L.useCallback(dn=>{},[]),Ge=xm(qe,M,A.ref,et),[Me,He]=L.useState(!1),lt=DM(),st=HY({props:i,muiFormControl:lt,states:["color","disabled","error","hiddenLabel","size","required","filled"]});st.focused=lt?lt.focused:Me,L.useEffect(()=>{!lt&&b&&Me&&(He(!1),Z&&Z())},[lt,b,Me,Z]);const Be=lt&<.onFilled,ot=lt&<.onEmpty,ct=L.useCallback(dn=>{oGe(dn)?Be&&Be():ot&&ot()},[Be,ot]);IS(()=>{Te&&ct({value:xe})},[xe,ct,Te]);const ze=dn=>{ee&&ee(dn),A.onFocus&&A.onFocus(dn),lt&<.onFocus?lt.onFocus(dn):He(!0)},Ke=dn=>{Z&&Z(dn),A.onBlur&&A.onBlur(dn),lt&<.onBlur?lt.onBlur(dn):He(!1)},$e=(dn,...Gi)=>{if(!Te){const $i=dn.target||qe.current;if($i==null)throw new Error(mW(1));ct({value:$i.value})}A.onChange&&A.onChange(dn,...Gi),te&&te(dn,...Gi)};L.useEffect(()=>{ct(qe.current)},[]);const tt=dn=>{qe.current&&dn.currentTarget===dn.target&&qe.current.focus(),G&&G(dn)};let vt=D,Ft=A;W&&vt==="input"&&(we?Ft={type:void 0,minRows:we,maxRows:we,...Ft}:Ft={type:void 0,maxRows:F,minRows:j,...Ft},vt=d1r);const _t=dn=>{ct(dn.animationName==="mui-auto-fill-cancel"?qe.current:{value:"x"})};L.useEffect(()=>{lt&<.setAdornedStart(!!he)},[lt,he]);const it={...i,color:st.color||"primary",disabled:st.disabled,endAdornment:_,error:st.error,focused:st.focused,formControl:lt,fullWidth:T,hiddenLabel:st.hiddenLabel,multiline:W,size:st.size,startAdornment:he,type:pe},St=p1r(it),Ot=ye.root||h.Root||Get,Jt=ce.root||p.root||{},fn=ye.input||h.Input||Ket;return Ft={...Ft,...ce.input??p.input},k.jsxs(L.Fragment,{children:[!w&&typeof OPn=="function"&&(MPn||(MPn=k.jsx(OPn,{}))),k.jsxs(Ot,{...Jt,ref:t,onClick:tt,...be,...!k9(Ot)&&{ownerState:{...it,...Jt.ownerState}},className:_i(St.root,Jt.className,c,ue&&"MuiInputBase-readOnly"),children:[he,k.jsx(zet.Provider,{value:null,children:k.jsx(fn,{"aria-invalid":st.error,"aria-describedby":r,autoComplete:o,autoFocus:l,defaultValue:m,disabled:st.disabled,id:I,onAnimationStart:_t,name:U,placeholder:se,readOnly:ue,required:st.required,rows:we,value:xe,onKeyDown:Q,onKeyUp:ie,type:pe,...Ft,...!k9(fn)&&{as:vt,ownerState:{...it,...Ft.ownerState}},ref:Ge,className:_i(St.input,Ft.className,ue&&"MuiInputBase-readOnly"),onBlur:Ke,onChange:$e,onFocus:ze})}),_,ne?ne({...st,startAdornment:he}):null]})]})});function g1r(n){return Po("MuiInput",n)}const _G={...mL,...Fo("MuiInput",["root","underline","input"])};function m1r(n){return Po("MuiOutlinedInput",n)}const $D={...mL,...Fo("MuiOutlinedInput",["root","notchedOutline","input"])};function b1r(n){return Po("MuiFilledInput",n)}const bL={...mL,...Fo("MuiFilledInput",["root","underline","input","adornedStart","adornedEnd","sizeSmall","multiline","hiddenLabel"])},pui=Ya(k.jsx("path",{d:"M7 10l5 5 5-5z"}));function v1r(n){return Po("MuiAutocomplete",n)}const qu=Fo("MuiAutocomplete",["root","expanded","fullWidth","focused","focusVisible","tag","tagSizeSmall","tagSizeMedium","hasPopupIcon","hasClearIcon","inputRoot","input","inputFocused","endAdornment","clearIndicator","popupIndicator","popupIndicatorOpen","popper","popperDisablePortal","paper","listbox","loading","noOptions","option","groupLabel","groupUl"]);var NPn,PPn;const w1r=n=>{const{classes:e,disablePortal:t,expanded:i,focused:r,fullWidth:o,hasClearIcon:l,hasPopupIcon:c,inputFocused:d,popupOpen:h,size:p}=n,m={root:["root",i&&"expanded",r&&"focused",o&&"fullWidth",l&&"hasClearIcon",c&&"hasPopupIcon"],inputRoot:["inputRoot"],input:["input",d&&"inputFocused"],tag:["tag",`tagSize${ri(p)}`],endAdornment:["endAdornment"],clearIndicator:["clearIndicator"],popupIndicator:["popupIndicator",h&&"popupIndicatorOpen"],popper:["popper",t&&"popperDisablePortal"],paper:["paper"],listbox:["listbox"],loading:["loading"],noOptions:["noOptions"],option:["option"],groupLabel:["groupLabel"],groupUl:["groupUl"]};return jo(m,v1r,e)},y1r=nn("div",{name:"MuiAutocomplete",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n,{fullWidth:i,hasClearIcon:r,hasPopupIcon:o,inputFocused:l,size:c}=t;return[{[`& .${qu.tag}`]:e.tag},{[`& .${qu.tag}`]:e[`tagSize${ri(c)}`]},{[`& .${qu.inputRoot}`]:e.inputRoot},{[`& .${qu.input}`]:e.input},{[`& .${qu.input}`]:l&&e.inputFocused},e.root,i&&e.fullWidth,o&&e.hasPopupIcon,r&&e.hasClearIcon]}})({[`&.${qu.focused} .${qu.clearIndicator}`]:{visibility:"visible"},"@media (pointer: fine)":{[`&:hover .${qu.clearIndicator}`]:{visibility:"visible"}},[`& .${qu.tag}`]:{margin:3,maxWidth:"calc(100% - 6px)"},[`& .${qu.inputRoot}`]:{[`.${qu.hasPopupIcon}&, .${qu.hasClearIcon}&`]:{paddingRight:30},[`.${qu.hasPopupIcon}.${qu.hasClearIcon}&`]:{paddingRight:56},[`& .${qu.input}`]:{width:0,minWidth:30}},[`& .${_G.root}`]:{paddingBottom:1,"& .MuiInput-input":{padding:"4px 4px 4px 0px"}},[`& .${_G.root}.${mL.sizeSmall}`]:{[`& .${_G.input}`]:{padding:"2px 4px 3px 0"}},[`& .${$D.root}`]:{padding:9,[`.${qu.hasPopupIcon}&, .${qu.hasClearIcon}&`]:{paddingRight:39},[`.${qu.hasPopupIcon}.${qu.hasClearIcon}&`]:{paddingRight:65},[`& .${qu.input}`]:{padding:"7.5px 4px 7.5px 5px"},[`& .${qu.endAdornment}`]:{right:9}},[`& .${$D.root}.${mL.sizeSmall}`]:{paddingTop:6,paddingBottom:6,paddingLeft:6,[`& .${qu.input}`]:{padding:"2.5px 4px 2.5px 8px"}},[`& .${bL.root}`]:{paddingTop:19,paddingLeft:8,[`.${qu.hasPopupIcon}&, .${qu.hasClearIcon}&`]:{paddingRight:39},[`.${qu.hasPopupIcon}.${qu.hasClearIcon}&`]:{paddingRight:65},[`& .${bL.input}`]:{padding:"7px 4px"},[`& .${qu.endAdornment}`]:{right:9}},[`& .${bL.root}.${mL.sizeSmall}`]:{paddingBottom:1,[`& .${bL.input}`]:{padding:"2.5px 4px"}},[`& .${mL.hiddenLabel}`]:{paddingTop:8},[`& .${bL.root}.${mL.hiddenLabel}`]:{paddingTop:0,paddingBottom:0,[`& .${qu.input}`]:{paddingTop:16,paddingBottom:17}},[`& .${bL.root}.${mL.hiddenLabel}.${mL.sizeSmall}`]:{[`& .${qu.input}`]:{paddingTop:8,paddingBottom:9}},[`& .${qu.input}`]:{flexGrow:1,textOverflow:"ellipsis",opacity:0},variants:[{props:{fullWidth:!0},style:{width:"100%"}},{props:{size:"small"},style:{[`& .${qu.tag}`]:{margin:2,maxWidth:"calc(100% - 4px)"}}},{props:{inputFocused:!0},style:{[`& .${qu.input}`]:{opacity:1}}},{props:{multiple:!0},style:{[`& .${qu.inputRoot}`]:{flexWrap:"wrap"}}}]}),_1r=nn("div",{name:"MuiAutocomplete",slot:"EndAdornment"})({position:"absolute",right:0,top:"50%",transform:"translate(0, -50%)"}),C1r=nn(Lo,{name:"MuiAutocomplete",slot:"ClearIndicator"})({marginRight:-2,padding:4,visibility:"hidden"}),S1r=nn(Lo,{name:"MuiAutocomplete",slot:"PopupIndicator",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.popupIndicator,t.popupOpen&&e.popupIndicatorOpen]}})({padding:2,marginRight:-2,variants:[{props:{popupOpen:!0},style:{transform:"rotate(180deg)"}}]}),x1r=nn(a8,{name:"MuiAutocomplete",slot:"Popper",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[{[`& .${qu.option}`]:e.option},e.popper,t.disablePortal&&e.popperDisablePortal]}})(Gs(({theme:n})=>({zIndex:(n.vars||n).zIndex.modal,variants:[{props:{disablePortal:!0},style:{position:"absolute"}}]}))),E1r=nn(Qf,{name:"MuiAutocomplete",slot:"Paper"})(Gs(({theme:n})=>({...n.typography.body1,overflow:"auto"}))),k1r=nn("div",{name:"MuiAutocomplete",slot:"Loading"})(Gs(({theme:n})=>({color:(n.vars||n).palette.text.secondary,padding:"14px 16px"}))),T1r=nn("div",{name:"MuiAutocomplete",slot:"NoOptions"})(Gs(({theme:n})=>({color:(n.vars||n).palette.text.secondary,padding:"14px 16px"}))),L1r=nn("ul",{name:"MuiAutocomplete",slot:"Listbox"})(Gs(({theme:n})=>({listStyle:"none",margin:0,padding:"8px 0",maxHeight:"40vh",overflow:"auto",position:"relative",[`& .${qu.option}`]:{minHeight:48,display:"flex",overflow:"hidden",justifyContent:"flex-start",alignItems:"center",cursor:"pointer",paddingTop:6,boxSizing:"border-box",outline:"0",WebkitTapHighlightColor:"transparent",paddingBottom:6,paddingLeft:16,paddingRight:16,[n.breakpoints.up("sm")]:{minHeight:"auto"},[`&.${qu.focused}`]:{backgroundColor:(n.vars||n).palette.action.hover,"@media (hover: none)":{backgroundColor:"transparent"}},'&[aria-disabled="true"]':{opacity:(n.vars||n).palette.action.disabledOpacity,pointerEvents:"none"},[`&.${qu.focusVisible}`]:{backgroundColor:(n.vars||n).palette.action.focus},'&[aria-selected="true"]':{backgroundColor:n.alpha((n.vars||n).palette.primary.main,(n.vars||n).palette.action.selectedOpacity),[`&.${qu.focused}`]:{backgroundColor:n.alpha((n.vars||n).palette.primary.main,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.hoverOpacity}`),"@media (hover: none)":{backgroundColor:(n.vars||n).palette.action.selected}},[`&.${qu.focusVisible}`]:{backgroundColor:n.alpha((n.vars||n).palette.primary.main,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.focusOpacity}`)}}}}))),D1r=nn(QOt,{name:"MuiAutocomplete",slot:"GroupLabel"})(Gs(({theme:n})=>({backgroundColor:(n.vars||n).palette.background.paper,top:-8}))),I1r=nn("ul",{name:"MuiAutocomplete",slot:"GroupUl"})({padding:0,[`& .${qu.option}`]:{paddingLeft:24}}),Yet=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiAutocomplete"}),{autoComplete:r=!1,autoHighlight:o=!1,autoSelect:l=!1,blurOnSelect:c=!1,ChipProps:d,className:h,clearIcon:p=NPn||(NPn=k.jsx(iui,{fontSize:"small"})),clearOnBlur:m=!i.freeSolo,clearOnEscape:b=!1,clearText:w="Clear",closeText:_="Close",componentsProps:x,defaultValue:T=i.multiple?[]:null,disableClearable:I=!1,disableCloseOnSelect:D=!1,disabled:A=!1,disabledItemsFocusable:M=!1,disableListWrap:O=!1,disablePortal:F=!1,filterOptions:j,filterSelectedOptions:W=!1,forcePopupIcon:U="auto",freeSolo:Z=!1,fullWidth:te=!1,getLimitTagsText:G=Dl=>`+${Dl}`,getOptionDisabled:ee,getOptionKey:Q,getOptionLabel:ie,isOptionEqualToValue:se,groupBy:ue,handleHomeEndKeys:ne=!i.freeSolo,id:we,includeInputInList:de=!1,inputValue:ce,limitTags:ye=-1,ListboxComponent:he,ListboxProps:pe,loading:me=!1,loadingText:be="Loading…",multiple:xe=!1,noOptionsText:Te="No options",onChange:qe,onClose:et,onHighlightChange:Ge,onInputChange:Me,onOpen:He,open:lt,openOnFocus:st=!1,openText:Be="Open",options:ot,PaperComponent:ct,PopperComponent:ze,popupIcon:Ke=PPn||(PPn=k.jsx(pui,{})),readOnly:$e=!1,renderGroup:tt,renderInput:vt,renderOption:Ft,renderTags:_t,renderValue:it,selectOnFocus:St=!i.freeSolo,size:Ot="medium",slots:Jt={},slotProps:fn={},value:dn,...Gi}=i,{getRootProps:$i,getInputProps:Dr,getInputLabelProps:ps,getPopupIndicatorProps:rn,getClearProps:xt,getItemProps:Ei,getListboxProps:gr,getOptionProps:ss,value:us,dirty:_r,expanded:uo,id:xs,popupOpen:Fs,focused:eo,focusedItem:Ri,anchorEl:Ls,setAnchorEl:Cr,inputValue:Sr,groupedOptions:os}=Hgr({...i,componentName:"Autocomplete"}),Ks=!I&&!A&&_r&&!$e,jt=(!Z||U===!0)&&U!==!1,{onMouseDown:sn}=Dr(),{ref:zn,...Oi}=gr(),gs=ie||(Dl=>Dl.label??Dl),ur={...i,disablePortal:F,expanded:uo,focused:eo,fullWidth:te,getOptionLabel:gs,hasClearIcon:Ks,hasPopupIcon:jt,inputFocused:Ri===-1,popupOpen:Fs,size:Ot},vs=w1r(ur),Ir={slots:{paper:ct,popper:ze,...Jt},slotProps:{chip:d,listbox:pe,...x,...fn}},[oo,Io]=_o("listbox",{elementType:L1r,externalForwardedProps:Ir,ownerState:ur,className:vs.listbox,additionalProps:Oi,ref:zn}),[zs,un]=_o("paper",{elementType:Qf,externalForwardedProps:Ir,ownerState:ur,className:vs.paper}),[bt,Cn]=_o("popper",{elementType:a8,externalForwardedProps:Ir,ownerState:ur,className:vs.popper,additionalProps:{disablePortal:F,style:{width:Ls?Ls.clientWidth:null},role:"presentation",anchorEl:Ls,open:Fs}});let Di;const Ni=Dl=>({className:vs.tag,disabled:A,...Ei(Dl)});if(xe?us.length>0&&(_t?Di=_t(us,Ni,ur):it?Di=it(us,Ni,ur):Di=us.map((Dl,Pu)=>{const{key:cd,...Ag}=Ni({index:Pu});return k.jsx(j_,{label:gs(Dl),size:Ot,...Ag,...Ir.slotProps.chip},cd)})):it&&us!=null&&(Di=it(us,Ni,ur)),ye>-1&&Array.isArray(Di)){const Dl=Di.length-ye;!eo&&Dl>0&&(Di=Di.splice(0,ye),Di.push(k.jsx("span",{className:vs.tag,children:G(Dl)},Di.length)))}const Es=tt||(Dl=>k.jsxs("li",{children:[k.jsx(D1r,{className:vs.groupLabel,ownerState:ur,component:"div",children:Dl.group}),k.jsx(I1r,{className:vs.groupUl,ownerState:ur,children:Dl.children})]},Dl.key)),Lu=Ft||((Dl,Pu)=>{const{key:cd,...Ag}=Dl;return k.jsx("li",{...Ag,children:gs(Pu)},cd)}),tu=(Dl,Pu)=>{const cd=ss({option:Dl,index:Pu});return Lu({...cd,className:vs.option},Dl,{selected:cd["aria-selected"],index:Pu,inputValue:Sr},ur)},Kd=Ir.slotProps.clearIndicator,ld=Ir.slotProps.popupIndicator;return k.jsxs(L.Fragment,{children:[k.jsx(y1r,{ref:t,className:_i(vs.root,h),ownerState:ur,...$i(Gi),children:vt({id:xs,disabled:A,fullWidth:i.fullWidth??!0,size:Ot==="small"?"small":void 0,InputLabelProps:ps(),InputProps:{ref:Cr,className:vs.inputRoot,startAdornment:Di,onMouseDown:Dl=>{Dl.target===Dl.currentTarget&&sn(Dl)},...(Ks||jt)&&{endAdornment:k.jsxs(_1r,{className:vs.endAdornment,ownerState:ur,children:[Ks?k.jsx(C1r,{...xt(),"aria-label":w,title:w,ownerState:ur,...Kd,className:_i(vs.clearIndicator,Kd?.className),children:p}):null,jt?k.jsx(S1r,{...rn(),disabled:A,"aria-label":Fs?_:Be,title:Fs?_:Be,ownerState:ur,...ld,className:_i(vs.popupIndicator,ld?.className),children:Ke}):null]})}},inputProps:{className:vs.input,disabled:A,readOnly:$e,...Dr()}})}),Ls?k.jsx(x1r,{as:bt,...Cn,children:k.jsxs(E1r,{as:zs,...un,children:[me&&os.length===0?k.jsx(k1r,{className:vs.loading,ownerState:ur,children:be}):null,os.length===0&&!Z&&!me?k.jsx(T1r,{className:vs.noOptions,ownerState:ur,role:"presentation",onMouseDown:Dl=>{Dl.preventDefault()},children:Te}):null,os.length>0?k.jsx(oo,{as:he,...Io,children:os.map((Dl,Pu)=>ue?Es({key:Dl.key,group:Dl.group,children:Dl.options.map((cd,Ag)=>tu(cd,Dl.index+Ag))}):tu(Dl,Pu))}):null]})}):null]})}),A1r=Ya(k.jsx("path",{d:"M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"}));function R1r(n){return Po("MuiAvatar",n)}Fo("MuiAvatar",["root","colorDefault","circular","rounded","square","img","fallback"]);const M1r=n=>{const{classes:e,variant:t,colorDefault:i}=n;return jo({root:["root",t,i&&"colorDefault"],img:["img"],fallback:["fallback"]},R1r,e)},O1r=nn("div",{name:"MuiAvatar",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.variant],t.colorDefault&&e.colorDefault]}})(Gs(({theme:n})=>({position:"relative",display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0,width:40,height:40,fontFamily:n.typography.fontFamily,fontSize:n.typography.pxToRem(20),lineHeight:1,borderRadius:"50%",overflow:"hidden",userSelect:"none",variants:[{props:{variant:"rounded"},style:{borderRadius:(n.vars||n).shape.borderRadius}},{props:{variant:"square"},style:{borderRadius:0}},{props:{colorDefault:!0},style:{color:(n.vars||n).palette.background.default,...n.vars?{backgroundColor:n.vars.palette.Avatar.defaultBg}:{backgroundColor:n.palette.grey[400],...n.applyStyles("dark",{backgroundColor:n.palette.grey[600]})}}}]}))),N1r=nn("img",{name:"MuiAvatar",slot:"Img"})({width:"100%",height:"100%",textAlign:"center",objectFit:"cover",color:"transparent",textIndent:1e4}),P1r=nn(A1r,{name:"MuiAvatar",slot:"Fallback"})({width:"75%",height:"75%"});function F1r({crossOrigin:n,referrerPolicy:e,src:t,srcSet:i}){const[r,o]=L.useState(!1);return L.useEffect(()=>{if(!t&&!i)return;o(!1);let l=!0;const c=new Image;return c.onload=()=>{l&&o("loaded")},c.onerror=()=>{l&&o("error")},c.crossOrigin=n,c.referrerPolicy=e,c.src=t,i&&(c.srcset=i),()=>{l=!1}},[n,e,t,i]),r}const j1r=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiAvatar"}),{alt:r,children:o,className:l,component:c="div",slots:d={},slotProps:h={},imgProps:p,sizes:m,src:b,srcSet:w,variant:_="circular",...x}=i;let T=null;const I={...i,component:c,variant:_},D=F1r({...p,...typeof h.img=="function"?h.img(I):h.img,src:b,srcSet:w}),A=b||w,M=A&&D!=="error";I.colorDefault=!M,delete I.ownerState;const O=M1r(I),[F,j]=_o("root",{ref:t,className:_i(O.root,l),elementType:O1r,externalForwardedProps:{slots:d,slotProps:h,component:c,...x},ownerState:I}),[W,U]=_o("img",{className:O.img,elementType:N1r,externalForwardedProps:{slots:d,slotProps:{img:{...p,...h.img}}},additionalProps:{alt:r,src:b,srcSet:w,sizes:m},ownerState:I}),[Z,te]=_o("fallback",{className:O.fallback,elementType:P1r,externalForwardedProps:{slots:d,slotProps:h},shouldForwardComponentProp:!0,ownerState:I});return M?T=k.jsx(W,{...U}):o||o===0?T=o:A&&r?T=r[0]:T=k.jsx(Z,{...te}),k.jsx(F,{...j,children:T})}),H1r={entering:{opacity:1},entered:{opacity:1}},tY=L.forwardRef(function(e,t){const i=Tf(),r={enter:i.transitions.duration.enteringScreen,exit:i.transitions.duration.leavingScreen},{addEndListener:o,appear:l=!0,children:c,easing:d,in:h,onEnter:p,onEntered:m,onEntering:b,onExit:w,onExited:_,onExiting:x,style:T,timeout:I=r,TransitionComponent:D=o8,...A}=e,M=L.useRef(null),O=xm(M,jY(c),t),F=Q=>ie=>{if(Q){const se=M.current;ie===void 0?Q(se):Q(se,ie)}},j=F(b),W=F((Q,ie)=>{Gzt(Q);const se=JK({style:T,timeout:I,easing:d},{mode:"enter"});Q.style.webkitTransition=i.transitions.create("opacity",se),Q.style.transition=i.transitions.create("opacity",se),p&&p(Q,ie)}),U=F(m),Z=F(x),te=F(Q=>{const ie=JK({style:T,timeout:I,easing:d},{mode:"exit"});Q.style.webkitTransition=i.transitions.create("opacity",ie),Q.style.transition=i.transitions.create("opacity",ie),w&&w(Q)}),G=F(_),ee=Q=>{o&&o(M.current,Q)};return k.jsx(D,{appear:l,in:h,nodeRef:M,onEnter:W,onEntered:U,onEntering:j,onExit:te,onExited:G,onExiting:Z,addEndListener:ee,timeout:I,...A,children:(Q,{ownerState:ie,...se})=>L.cloneElement(c,{style:{opacity:0,visibility:Q==="exited"&&!h?"hidden":void 0,...H1r[Q],...T,...c.props.style},ref:O,...se})})});function B1r(n){return Po("MuiBackdrop",n)}Fo("MuiBackdrop",["root","invisible"]);const W1r=n=>{const{classes:e,invisible:t}=n;return jo({root:["root",t&&"invisible"]},B1r,e)},V1r=nn("div",{name:"MuiBackdrop",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.invisible&&e.invisible]}})({position:"fixed",display:"flex",alignItems:"center",justifyContent:"center",right:0,bottom:0,top:0,left:0,backgroundColor:"rgba(0, 0, 0, 0.5)",WebkitTapHighlightColor:"transparent",variants:[{props:{invisible:!0},style:{backgroundColor:"transparent"}}]}),oUt=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiBackdrop"}),{children:r,className:o,component:l="div",invisible:c=!1,open:d,components:h={},componentsProps:p={},slotProps:m={},slots:b={},TransitionComponent:w,transitionDuration:_,...x}=i,T={...i,component:l,invisible:c},I=W1r(T),D={transition:w,root:h.Root,...b},A={...p,...m},M={component:l,slots:D,slotProps:A},[O,F]=_o("root",{elementType:V1r,externalForwardedProps:M,className:_i(I.root,o),ownerState:T}),[j,W]=_o("transition",{elementType:tY,externalForwardedProps:M,ownerState:T});return k.jsx(j,{in:d,timeout:_,...x,...W,children:k.jsx(O,{"aria-hidden":!0,...F,ref:t,children:r})})});function $1r(n){const{badgeContent:e,invisible:t=!1,max:i=99,showZero:r=!1}=n,o=Xzt({badgeContent:e,max:i});let l=t;t===!1&&e===0&&!r&&(l=!0);const{badgeContent:c,max:d=i}=l?o:n,h=c&&Number(c)>d?`${d}+`:c;return{badgeContent:c,invisible:l,max:d,displayValue:h}}function z1r(n){return Po("MuiBadge",n)}const U1r=Fo("MuiBadge",["root","badge","dot","standard","anchorOriginTopRight","anchorOriginBottomRight","anchorOriginTopLeft","anchorOriginBottomLeft","invisible","colorError","colorInfo","colorPrimary","colorSecondary","colorSuccess","colorWarning","overlapRectangular","overlapCircular","anchorOriginTopLeftCircular","anchorOriginTopLeftRectangular","anchorOriginTopRightCircular","anchorOriginTopRightRectangular","anchorOriginBottomLeftCircular","anchorOriginBottomLeftRectangular","anchorOriginBottomRightCircular","anchorOriginBottomRightRectangular"]),PEt=10,FEt=4,q1r=n=>{const{color:e,anchorOrigin:t,invisible:i,overlap:r,variant:o,classes:l={}}=n,c={root:["root"],badge:["badge",o,i&&"invisible",`anchorOrigin${ri(t.vertical)}${ri(t.horizontal)}`,`anchorOrigin${ri(t.vertical)}${ri(t.horizontal)}${ri(r)}`,`overlap${ri(r)}`,e!=="default"&&`color${ri(e)}`]};return jo(c,z1r,l)},G1r=nn("span",{name:"MuiBadge",slot:"Root"})({position:"relative",display:"inline-flex",verticalAlign:"middle",flexShrink:0}),K1r=nn("span",{name:"MuiBadge",slot:"Badge",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.badge,e[t.variant],e[`anchorOrigin${ri(t.anchorOrigin.vertical)}${ri(t.anchorOrigin.horizontal)}${ri(t.overlap)}`],t.color!=="default"&&e[`color${ri(t.color)}`],t.invisible&&e.invisible]}})(Gs(({theme:n})=>({display:"flex",flexDirection:"row",flexWrap:"wrap",justifyContent:"center",alignContent:"center",alignItems:"center",position:"absolute",boxSizing:"border-box",fontFamily:n.typography.fontFamily,fontWeight:n.typography.fontWeightMedium,fontSize:n.typography.pxToRem(12),minWidth:PEt*2,lineHeight:1,padding:"0 6px",height:PEt*2,borderRadius:PEt,zIndex:1,transition:n.transitions.create("transform",{easing:n.transitions.easing.easeInOut,duration:n.transitions.duration.enteringScreen}),variants:[...Object.entries(n.palette).filter(Vh(["contrastText"])).map(([e])=>({props:{color:e},style:{backgroundColor:(n.vars||n).palette[e].main,color:(n.vars||n).palette[e].contrastText}})),{props:{variant:"dot"},style:{borderRadius:FEt,height:FEt*2,minWidth:FEt*2,padding:0}},{props:{invisible:!0},style:{transition:n.transitions.create("transform",{easing:n.transitions.easing.easeInOut,duration:n.transitions.duration.leavingScreen})}},{style:({ownerState:e})=>{const{vertical:t,horizontal:i}=e.anchorOrigin,r=e.overlap==="circular"?"14%":0;return{"--Badge-translateX":i==="right"?"50%":"-50%","--Badge-translateY":t==="top"?"-50%":"50%",top:t==="top"?r:"initial",bottom:t==="bottom"?r:"initial",right:i==="right"?r:"initial",left:i==="left"?r:"initial",transform:"scale(1) translate(var(--Badge-translateX), var(--Badge-translateY))",transformOrigin:`${i==="right"?"100%":"0%"} ${t==="top"?"0%":"100%"}`,[`&.${U1r.invisible}`]:{transform:"scale(0) translate(var(--Badge-translateX), var(--Badge-translateY))"}}}}]})));function FPn(n){return{vertical:n?.vertical??"top",horizontal:n?.horizontal??"right"}}const Y1r=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiBadge"}),{anchorOrigin:r,className:o,classes:l,component:c,components:d={},componentsProps:h={},children:p,overlap:m="rectangular",color:b="default",invisible:w=!1,max:_=99,badgeContent:x,slots:T,slotProps:I,showZero:D=!1,variant:A="standard",...M}=i,{badgeContent:O,invisible:F,max:j,displayValue:W}=$1r({max:_,invisible:w,badgeContent:x,showZero:D}),U=Xzt({anchorOrigin:FPn(r),color:b,overlap:m,variant:A,badgeContent:x}),Z=F||O==null&&A!=="dot",{color:te=b,overlap:G=m,anchorOrigin:ee,variant:Q=A}=Z?U:i,ie=FPn(ee),se=Q!=="dot"?W:void 0,ue={...i,badgeContent:O,invisible:Z,max:j,displayValue:se,showZero:D,anchorOrigin:ie,color:te,overlap:G,variant:Q},ne=q1r(ue),we={slots:{root:T?.root??d.Root,badge:T?.badge??d.Badge},slotProps:{root:I?.root??h.root,badge:I?.badge??h.badge}},[de,ce]=_o("root",{elementType:G1r,externalForwardedProps:{...we,...M},ownerState:ue,className:_i(ne.root,o),ref:t,additionalProps:{as:c}}),[ye,he]=_o("badge",{elementType:K1r,externalForwardedProps:we,ownerState:ue,className:ne.badge});return k.jsxs(de,{...ce,children:[p,k.jsx(ye,{...he,children:se})]})}),Z1r=Fo("MuiBox",["root"]),X1r=$1e(),Ie=uci({themeId:v3,defaultTheme:X1r,defaultClassName:Z1r.root,generateClassName:Lzt.generate}),Q1r=Ya(k.jsx("path",{d:"M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"})),J1r=nn(D3,{name:"MuiBreadcrumbCollapsed"})(Gs(({theme:n})=>({display:"flex",marginLeft:`calc(${n.spacing(1)} * 0.5)`,marginRight:`calc(${n.spacing(1)} * 0.5)`,...n.palette.mode==="light"?{backgroundColor:n.palette.grey[100],color:n.palette.grey[700]}:{backgroundColor:n.palette.grey[700],color:n.palette.grey[100]},borderRadius:2,"&:hover, &:focus":{...n.palette.mode==="light"?{backgroundColor:n.palette.grey[200]}:{backgroundColor:n.palette.grey[600]}},"&:active":{boxShadow:n.shadows[0],...n.palette.mode==="light"?{backgroundColor:o6e(n.palette.grey[200],.12)}:{backgroundColor:o6e(n.palette.grey[600],.12)}}}))),e0r=nn(Q1r)({width:24,height:16});function t0r(n){const{slots:e={},slotProps:t={},...i}=n,r=n;return k.jsx("li",{children:k.jsx(J1r,{focusRipple:!0,...i,ownerState:r,children:k.jsx(e0r,{as:e.CollapsedIcon,ownerState:r,...t.collapsedIcon})})})}function n0r(n){return Po("MuiBreadcrumbs",n)}const i0r=Fo("MuiBreadcrumbs",["root","ol","li","separator"]),r0r=n=>{const{classes:e}=n;return jo({root:["root"],li:["li"],ol:["ol"],separator:["separator"]},n0r,e)},s0r=nn(ei,{name:"MuiBreadcrumbs",slot:"Root",overridesResolver:(n,e)=>[{[`& .${i0r.li}`]:e.li},e.root]})({}),o0r=nn("ol",{name:"MuiBreadcrumbs",slot:"Ol"})({display:"flex",flexWrap:"wrap",alignItems:"center",padding:0,margin:0,listStyle:"none"}),a0r=nn("li",{name:"MuiBreadcrumbs",slot:"Separator"})({display:"flex",userSelect:"none",marginLeft:8,marginRight:8});function l0r(n,e,t,i){return n.reduce((r,o,l)=>(l{const W=()=>{T(!0);const U=M.current.querySelector("a[href],button,[tabindex]");U&&U.focus()};return m+p>=j.length?j:[...j.slice(0,m),k.jsx(t0r,{"aria-label":h,slots:{CollapsedIcon:c.CollapsedIcon},slotProps:{collapsedIcon:A},onClick:W},"ellipsis"),...j.slice(j.length-p,j.length)]},F=L.Children.toArray(r).filter(j=>L.isValidElement(j)).map((j,W)=>k.jsx("li",{className:D.li,children:j},`child-${W}`));return k.jsx(s0r,{ref:t,component:l,color:"textSecondary",className:_i(D.root,o),ownerState:I,..._,children:k.jsx(o0r,{className:D.ol,ref:M,ownerState:I,children:l0r(x||b&&F.length<=b?F:O(F),D.separator,w,I)})})});function u0r(n){return Po("MuiButton",n)}const hte=Fo("MuiButton",["root","text","textInherit","textPrimary","textSecondary","textSuccess","textError","textInfo","textWarning","outlined","outlinedInherit","outlinedPrimary","outlinedSecondary","outlinedSuccess","outlinedError","outlinedInfo","outlinedWarning","contained","containedInherit","containedPrimary","containedSecondary","containedSuccess","containedError","containedInfo","containedWarning","disableElevation","focusVisible","disabled","colorInherit","colorPrimary","colorSecondary","colorSuccess","colorError","colorInfo","colorWarning","textSizeSmall","textSizeMedium","textSizeLarge","outlinedSizeSmall","outlinedSizeMedium","outlinedSizeLarge","containedSizeSmall","containedSizeMedium","containedSizeLarge","sizeMedium","sizeSmall","sizeLarge","fullWidth","startIcon","endIcon","icon","iconSizeSmall","iconSizeMedium","iconSizeLarge","loading","loadingWrapper","loadingIconPlaceholder","loadingIndicator","loadingPositionCenter","loadingPositionStart","loadingPositionEnd"]),gui=L.createContext({}),mui=L.createContext(void 0),d0r=n=>{const{color:e,disableElevation:t,fullWidth:i,size:r,variant:o,loading:l,loadingPosition:c,classes:d}=n,h={root:["root",l&&"loading",o,`${o}${ri(e)}`,`size${ri(r)}`,`${o}Size${ri(r)}`,`color${ri(e)}`,t&&"disableElevation",i&&"fullWidth",l&&`loadingPosition${ri(c)}`],startIcon:["icon","startIcon",`iconSize${ri(r)}`],endIcon:["icon","endIcon",`iconSize${ri(r)}`],loadingIndicator:["loadingIndicator"],loadingWrapper:["loadingWrapper"]},p=jo(h,u0r,d);return{...d,...p}},bui=[{props:{size:"small"},style:{"& > *:nth-of-type(1)":{fontSize:18}}},{props:{size:"medium"},style:{"& > *:nth-of-type(1)":{fontSize:20}}},{props:{size:"large"},style:{"& > *:nth-of-type(1)":{fontSize:22}}}],h0r=nn(D3,{shouldForwardProp:n=>$_(n)||n==="classes",name:"MuiButton",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.variant],e[`${t.variant}${ri(t.color)}`],e[`size${ri(t.size)}`],e[`${t.variant}Size${ri(t.size)}`],t.color==="inherit"&&e.colorInherit,t.disableElevation&&e.disableElevation,t.fullWidth&&e.fullWidth,t.loading&&e.loading]}})(Gs(({theme:n})=>{const e=n.palette.mode==="light"?n.palette.grey[300]:n.palette.grey[800],t=n.palette.mode==="light"?n.palette.grey.A100:n.palette.grey[700];return{...n.typography.button,minWidth:64,padding:"6px 16px",border:0,borderRadius:(n.vars||n).shape.borderRadius,transition:n.transitions.create(["background-color","box-shadow","border-color","color"],{duration:n.transitions.duration.short}),"&:hover":{textDecoration:"none"},[`&.${hte.disabled}`]:{color:(n.vars||n).palette.action.disabled},variants:[{props:{variant:"contained"},style:{color:"var(--variant-containedColor)",backgroundColor:"var(--variant-containedBg)",boxShadow:(n.vars||n).shadows[2],"&:hover":{boxShadow:(n.vars||n).shadows[4],"@media (hover: none)":{boxShadow:(n.vars||n).shadows[2]}},"&:active":{boxShadow:(n.vars||n).shadows[8]},[`&.${hte.focusVisible}`]:{boxShadow:(n.vars||n).shadows[6]},[`&.${hte.disabled}`]:{color:(n.vars||n).palette.action.disabled,boxShadow:(n.vars||n).shadows[0],backgroundColor:(n.vars||n).palette.action.disabledBackground}}},{props:{variant:"outlined"},style:{padding:"5px 15px",border:"1px solid currentColor",borderColor:"var(--variant-outlinedBorder, currentColor)",backgroundColor:"var(--variant-outlinedBg)",color:"var(--variant-outlinedColor)",[`&.${hte.disabled}`]:{border:`1px solid ${(n.vars||n).palette.action.disabledBackground}`}}},{props:{variant:"text"},style:{padding:"6px 8px",color:"var(--variant-textColor)",backgroundColor:"var(--variant-textBg)"}},...Object.entries(n.palette).filter(Vh()).map(([i])=>({props:{color:i},style:{"--variant-textColor":(n.vars||n).palette[i].main,"--variant-outlinedColor":(n.vars||n).palette[i].main,"--variant-outlinedBorder":n.alpha((n.vars||n).palette[i].main,.5),"--variant-containedColor":(n.vars||n).palette[i].contrastText,"--variant-containedBg":(n.vars||n).palette[i].main,"@media (hover: hover)":{"&:hover":{"--variant-containedBg":(n.vars||n).palette[i].dark,"--variant-textBg":n.alpha((n.vars||n).palette[i].main,(n.vars||n).palette.action.hoverOpacity),"--variant-outlinedBorder":(n.vars||n).palette[i].main,"--variant-outlinedBg":n.alpha((n.vars||n).palette[i].main,(n.vars||n).palette.action.hoverOpacity)}}}})),{props:{color:"inherit"},style:{color:"inherit",borderColor:"currentColor","--variant-containedBg":n.vars?n.vars.palette.Button.inheritContainedBg:e,"@media (hover: hover)":{"&:hover":{"--variant-containedBg":n.vars?n.vars.palette.Button.inheritContainedHoverBg:t,"--variant-textBg":n.alpha((n.vars||n).palette.text.primary,(n.vars||n).palette.action.hoverOpacity),"--variant-outlinedBg":n.alpha((n.vars||n).palette.text.primary,(n.vars||n).palette.action.hoverOpacity)}}}},{props:{size:"small",variant:"text"},style:{padding:"4px 5px",fontSize:n.typography.pxToRem(13)}},{props:{size:"large",variant:"text"},style:{padding:"8px 11px",fontSize:n.typography.pxToRem(15)}},{props:{size:"small",variant:"outlined"},style:{padding:"3px 9px",fontSize:n.typography.pxToRem(13)}},{props:{size:"large",variant:"outlined"},style:{padding:"7px 21px",fontSize:n.typography.pxToRem(15)}},{props:{size:"small",variant:"contained"},style:{padding:"4px 10px",fontSize:n.typography.pxToRem(13)}},{props:{size:"large",variant:"contained"},style:{padding:"8px 22px",fontSize:n.typography.pxToRem(15)}},{props:{disableElevation:!0},style:{boxShadow:"none","&:hover":{boxShadow:"none"},[`&.${hte.focusVisible}`]:{boxShadow:"none"},"&:active":{boxShadow:"none"},[`&.${hte.disabled}`]:{boxShadow:"none"}}},{props:{fullWidth:!0},style:{width:"100%"}},{props:{loadingPosition:"center"},style:{transition:n.transitions.create(["background-color","box-shadow","border-color"],{duration:n.transitions.duration.short}),[`&.${hte.loading}`]:{color:"transparent"}}}]}})),f0r=nn("span",{name:"MuiButton",slot:"StartIcon",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.startIcon,t.loading&&e.startIconLoadingStart,e[`iconSize${ri(t.size)}`]]}})(({theme:n})=>({display:"inherit",marginRight:8,marginLeft:-4,variants:[{props:{size:"small"},style:{marginLeft:-2}},{props:{loadingPosition:"start",loading:!0},style:{transition:n.transitions.create(["opacity"],{duration:n.transitions.duration.short}),opacity:0}},{props:{loadingPosition:"start",loading:!0,fullWidth:!0},style:{marginRight:-8}},...bui]})),p0r=nn("span",{name:"MuiButton",slot:"EndIcon",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.endIcon,t.loading&&e.endIconLoadingEnd,e[`iconSize${ri(t.size)}`]]}})(({theme:n})=>({display:"inherit",marginRight:-4,marginLeft:8,variants:[{props:{size:"small"},style:{marginRight:-2}},{props:{loadingPosition:"end",loading:!0},style:{transition:n.transitions.create(["opacity"],{duration:n.transitions.duration.short}),opacity:0}},{props:{loadingPosition:"end",loading:!0,fullWidth:!0},style:{marginLeft:-8}},...bui]})),g0r=nn("span",{name:"MuiButton",slot:"LoadingIndicator"})(({theme:n})=>({display:"none",position:"absolute",visibility:"visible",variants:[{props:{loading:!0},style:{display:"flex"}},{props:{loadingPosition:"start"},style:{left:14}},{props:{loadingPosition:"start",size:"small"},style:{left:10}},{props:{variant:"text",loadingPosition:"start"},style:{left:6}},{props:{loadingPosition:"center"},style:{left:"50%",transform:"translate(-50%)",color:(n.vars||n).palette.action.disabled}},{props:{loadingPosition:"end"},style:{right:14}},{props:{loadingPosition:"end",size:"small"},style:{right:10}},{props:{variant:"text",loadingPosition:"end"},style:{right:6}},{props:{loadingPosition:"start",fullWidth:!0},style:{position:"relative",left:-10}},{props:{loadingPosition:"end",fullWidth:!0},style:{position:"relative",right:-10}}]})),jPn=nn("span",{name:"MuiButton",slot:"LoadingIconPlaceholder"})({display:"inline-block",width:"1em",height:"1em"}),cs=L.forwardRef(function(e,t){const i=L.useContext(gui),r=L.useContext(mui),o=s6e(i,e),l=Vo({props:o,name:"MuiButton"}),{children:c,color:d="primary",component:h="button",className:p,disabled:m=!1,disableElevation:b=!1,disableFocusRipple:w=!1,endIcon:_,focusVisibleClassName:x,fullWidth:T=!1,id:I,loading:D=null,loadingIndicator:A,loadingPosition:M="center",size:O="medium",startIcon:F,type:j,variant:W="text",...U}=l,Z=KW(I),te=A??k.jsx(N1,{"aria-labelledby":Z,color:"inherit",size:16}),G={...l,color:d,component:h,disabled:m,disableElevation:b,disableFocusRipple:w,fullWidth:T,loading:D,loadingIndicator:te,loadingPosition:M,size:O,type:j,variant:W},ee=d0r(G),Q=(F||D&&M==="start")&&k.jsx(f0r,{className:ee.startIcon,ownerState:G,children:F||k.jsx(jPn,{className:ee.loadingIconPlaceholder,ownerState:G})}),ie=(_||D&&M==="end")&&k.jsx(p0r,{className:ee.endIcon,ownerState:G,children:_||k.jsx(jPn,{className:ee.loadingIconPlaceholder,ownerState:G})}),se=r||"",ue=typeof D=="boolean"?k.jsx("span",{className:ee.loadingWrapper,style:{display:"contents"},children:D&&k.jsx(g0r,{className:ee.loadingIndicator,ownerState:G,children:te})}):null;return k.jsxs(h0r,{ownerState:G,className:_i(i.className,ee.root,p,se),component:h,disabled:m||D,focusRipple:!w,focusVisibleClassName:_i(ee.focusVisible,x),ref:t,type:j,id:D?Z:I,...U,classes:ee,children:[Q,M!=="end"&&ue,c,M==="end"&&ue,ie]})});function m0r(n){return L.Children.toArray(n).filter(e=>L.isValidElement(e))}function b0r(n){return Po("MuiButtonGroup",n)}const Cu=Fo("MuiButtonGroup",["root","contained","outlined","text","disableElevation","disabled","firstButton","fullWidth","horizontal","vertical","colorPrimary","colorSecondary","grouped","groupedHorizontal","groupedVertical","groupedText","groupedTextHorizontal","groupedTextVertical","groupedTextPrimary","groupedTextSecondary","groupedOutlined","groupedOutlinedHorizontal","groupedOutlinedVertical","groupedOutlinedPrimary","groupedOutlinedSecondary","groupedContained","groupedContainedHorizontal","groupedContainedVertical","groupedContainedPrimary","groupedContainedSecondary","lastButton","middleButton"]),v0r=(n,e)=>{const{ownerState:t}=n;return[{[`& .${Cu.grouped}`]:e.grouped},{[`& .${Cu.grouped}`]:e[`grouped${ri(t.orientation)}`]},{[`& .${Cu.grouped}`]:e[`grouped${ri(t.variant)}`]},{[`& .${Cu.grouped}`]:e[`grouped${ri(t.variant)}${ri(t.orientation)}`]},{[`& .${Cu.grouped}`]:e[`grouped${ri(t.variant)}${ri(t.color)}`]},{[`& .${Cu.firstButton}`]:e.firstButton},{[`& .${Cu.lastButton}`]:e.lastButton},{[`& .${Cu.middleButton}`]:e.middleButton},e.root,e[t.variant],t.disableElevation===!0&&e.disableElevation,t.fullWidth&&e.fullWidth,t.orientation==="vertical"&&e.vertical]},w0r=n=>{const{classes:e,color:t,disabled:i,disableElevation:r,fullWidth:o,orientation:l,variant:c}=n,d={root:["root",c,l,o&&"fullWidth",r&&"disableElevation",`color${ri(t)}`],grouped:["grouped",`grouped${ri(l)}`,`grouped${ri(c)}`,`grouped${ri(c)}${ri(l)}`,`grouped${ri(c)}${ri(t)}`,i&&"disabled"],firstButton:["firstButton"],lastButton:["lastButton"],middleButton:["middleButton"]};return jo(d,b0r,e)},y0r=nn("div",{name:"MuiButtonGroup",slot:"Root",overridesResolver:v0r})(Gs(({theme:n})=>({display:"inline-flex",borderRadius:(n.vars||n).shape.borderRadius,variants:[{props:{variant:"contained"},style:{boxShadow:(n.vars||n).shadows[2],[`& .${Cu.grouped}`]:{boxShadow:"none","&:hover":{boxShadow:"none"}}}},{props:{disableElevation:!0},style:{boxShadow:"none"}},{props:{fullWidth:!0},style:{width:"100%"}},{props:{orientation:"vertical"},style:{flexDirection:"column",[`& .${Cu.lastButton},& .${Cu.middleButton}`]:{borderTopRightRadius:0,borderTopLeftRadius:0},[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderBottomRightRadius:0,borderBottomLeftRadius:0}}},{props:{orientation:"horizontal"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderTopRightRadius:0,borderBottomRightRadius:0},[`& .${Cu.lastButton},& .${Cu.middleButton}`]:{borderTopLeftRadius:0,borderBottomLeftRadius:0}}},{props:{variant:"text",orientation:"horizontal"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderRight:n.vars?`1px solid ${n.alpha(n.vars.palette.common.onBackground,.23)}`:`1px solid ${n.palette.mode==="light"?"rgba(0, 0, 0, 0.23)":"rgba(255, 255, 255, 0.23)"}`,[`&.${Cu.disabled}`]:{borderRight:`1px solid ${(n.vars||n).palette.action.disabled}`}}}},{props:{variant:"text",orientation:"vertical"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderBottom:n.vars?`1px solid ${n.alpha(n.vars.palette.common.onBackground,.23)}`:`1px solid ${n.palette.mode==="light"?"rgba(0, 0, 0, 0.23)":"rgba(255, 255, 255, 0.23)"}`,[`&.${Cu.disabled}`]:{borderBottom:`1px solid ${(n.vars||n).palette.action.disabled}`}}}},...Object.entries(n.palette).filter(Vh()).flatMap(([e])=>[{props:{variant:"text",color:e},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderColor:n.alpha((n.vars||n).palette[e].main,.5)}}}]),{props:{variant:"outlined",orientation:"horizontal"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderRightColor:"transparent","&:hover":{borderRightColor:"currentColor"}},[`& .${Cu.lastButton},& .${Cu.middleButton}`]:{marginLeft:-1}}},{props:{variant:"outlined",orientation:"vertical"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderBottomColor:"transparent","&:hover":{borderBottomColor:"currentColor"}},[`& .${Cu.lastButton},& .${Cu.middleButton}`]:{marginTop:-1}}},{props:{variant:"contained",orientation:"horizontal"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderRight:`1px solid ${(n.vars||n).palette.grey[400]}`,[`&.${Cu.disabled}`]:{borderRight:`1px solid ${(n.vars||n).palette.action.disabled}`}}}},{props:{variant:"contained",orientation:"vertical"},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderBottom:`1px solid ${(n.vars||n).palette.grey[400]}`,[`&.${Cu.disabled}`]:{borderBottom:`1px solid ${(n.vars||n).palette.action.disabled}`}}}},...Object.entries(n.palette).filter(Vh(["dark"])).map(([e])=>({props:{variant:"contained",color:e},style:{[`& .${Cu.firstButton},& .${Cu.middleButton}`]:{borderColor:(n.vars||n).palette[e].dark}}}))],[`& .${Cu.grouped}`]:{minWidth:40}}))),vui=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiButtonGroup"}),{children:r,className:o,color:l="primary",component:c="div",disabled:d=!1,disableElevation:h=!1,disableFocusRipple:p=!1,disableRipple:m=!1,fullWidth:b=!1,orientation:w="horizontal",size:_="medium",variant:x="outlined",...T}=i,I={...i,color:l,component:c,disabled:d,disableElevation:h,disableFocusRipple:p,disableRipple:m,fullWidth:b,orientation:w,size:_,variant:x},D=w0r(I),A=L.useMemo(()=>({className:D.grouped,color:l,disabled:d,disableElevation:h,disableFocusRipple:p,disableRipple:m,fullWidth:b,size:_,variant:x}),[l,d,h,p,m,b,_,x,D.grouped]),M=m0r(r),O=M.length,F=j=>{const W=j===0,U=j===O-1;return W&&U?"":W?D.firstButton:U?D.lastButton:D.middleButton};return k.jsx(y0r,{as:c,role:"group",className:_i(D.root,o),ref:t,ownerState:I,...T,children:k.jsx(gui.Provider,{value:A,children:M.map((j,W)=>k.jsx(mui.Provider,{value:F(W),children:j},W))})})});function _0r(n){return Po("PrivateSwitchBase",n)}Fo("PrivateSwitchBase",["root","checked","disabled","input","edgeStart","edgeEnd"]);const C0r=n=>{const{classes:e,checked:t,disabled:i,edge:r}=n,o={root:["root",t&&"checked",i&&"disabled",r&&`edge${ri(r)}`],input:["input"]};return jo(o,_0r,e)},S0r=nn(D3,{name:"MuiSwitchBase"})({padding:9,borderRadius:"50%",variants:[{props:{edge:"start",size:"small"},style:{marginLeft:-3}},{props:({edge:n,ownerState:e})=>n==="start"&&e.size!=="small",style:{marginLeft:-12}},{props:{edge:"end",size:"small"},style:{marginRight:-3}},{props:({edge:n,ownerState:e})=>n==="end"&&e.size!=="small",style:{marginRight:-12}}]}),x0r=nn("input",{name:"MuiSwitchBase",shouldForwardProp:$_})({cursor:"inherit",position:"absolute",opacity:0,width:"100%",height:"100%",top:0,left:0,margin:0,padding:0,zIndex:1}),aUt=L.forwardRef(function(e,t){const{autoFocus:i,checked:r,checkedIcon:o,defaultChecked:l,disabled:c,disableFocusRipple:d=!1,edge:h=!1,icon:p,id:m,inputProps:b,inputRef:w,name:_,onBlur:x,onChange:T,onFocus:I,readOnly:D,required:A=!1,tabIndex:M,type:O,value:F,slots:j={},slotProps:W={},...U}=e,[Z,te]=E9({controlled:r,default:!!l,name:"SwitchBase",state:"checked"}),G=DM(),ee=me=>{I&&I(me),G&&G.onFocus&&G.onFocus(me)},Q=me=>{x&&x(me),G&&G.onBlur&&G.onBlur(me)},ie=me=>{if(me.nativeEvent.defaultPrevented||D)return;const be=me.target.checked;te(be),T&&T(me,be)};let se=c;G&&typeof se>"u"&&(se=G.disabled);const ue=O==="checkbox"||O==="radio",ne={...e,checked:Z,disabled:se,disableFocusRipple:d,edge:h},we=C0r(ne),de={slots:j,slotProps:{input:b,...W}},[ce,ye]=_o("root",{ref:t,elementType:S0r,className:we.root,shouldForwardComponentProp:!0,externalForwardedProps:{...de,component:"span",...U},getSlotProps:me=>({...me,onFocus:be=>{me.onFocus?.(be),ee(be)},onBlur:be=>{me.onBlur?.(be),Q(be)}}),ownerState:ne,additionalProps:{centerRipple:!0,focusRipple:!d,role:void 0,tabIndex:null}}),[he,pe]=_o("input",{ref:w,elementType:x0r,className:we.input,externalForwardedProps:de,getSlotProps:me=>({...me,onChange:be=>{me.onChange?.(be),ie(be)}}),ownerState:ne,additionalProps:{autoFocus:i,checked:r,defaultChecked:l,disabled:se,id:ue?m:void 0,name:_,readOnly:D,required:A,tabIndex:M,type:O,...O==="checkbox"&&F===void 0?{}:{value:F}}});return k.jsxs(ce,{...ye,children:[k.jsx(he,{...pe}),Z?o:p]})}),E0r=Ya(k.jsx("path",{d:"M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"})),k0r=Ya(k.jsx("path",{d:"M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"})),T0r=Ya(k.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10H7v-2h10v2z"}));function L0r(n){return Po("MuiCheckbox",n)}const jEt=Fo("MuiCheckbox",["root","checked","disabled","indeterminate","colorPrimary","colorSecondary","sizeSmall","sizeMedium"]),D0r=n=>{const{classes:e,indeterminate:t,color:i,size:r}=n,o={root:["root",t&&"indeterminate",`color${ri(i)}`,`size${ri(r)}`]},l=jo(o,L0r,e);return{...e,...l}},I0r=nn(aUt,{shouldForwardProp:n=>$_(n)||n==="classes",name:"MuiCheckbox",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.indeterminate&&e.indeterminate,e[`size${ri(t.size)}`],t.color!=="default"&&e[`color${ri(t.color)}`]]}})(Gs(({theme:n})=>({color:(n.vars||n).palette.text.secondary,variants:[{props:{color:"default",disableRipple:!1},style:{"&:hover":{backgroundColor:n.alpha((n.vars||n).palette.action.active,(n.vars||n).palette.action.hoverOpacity)}}},...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e,disableRipple:!1},style:{"&:hover":{backgroundColor:n.alpha((n.vars||n).palette[e].main,(n.vars||n).palette.action.hoverOpacity)}}})),...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{[`&.${jEt.checked}, &.${jEt.indeterminate}`]:{color:(n.vars||n).palette[e].main},[`&.${jEt.disabled}`]:{color:(n.vars||n).palette.action.disabled}}})),{props:{disableRipple:!1},style:{"&:hover":{"@media (hover: none)":{backgroundColor:"transparent"}}}}]}))),A0r=k.jsx(k0r,{}),R0r=k.jsx(E0r,{}),M0r=k.jsx(T0r,{}),Pfe=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiCheckbox"}),{checkedIcon:r=A0r,color:o="primary",icon:l=R0r,indeterminate:c=!1,indeterminateIcon:d=M0r,inputProps:h,size:p="medium",disableRipple:m=!1,className:b,slots:w={},slotProps:_={},...x}=i,T=c?d:l,I=c?d:r,D={...i,disableRipple:m,color:o,indeterminate:c,size:p},A=D0r(D),M=_.input??h,[O,F]=_o("root",{ref:t,elementType:I0r,className:_i(A.root,b),shouldForwardComponentProp:!0,externalForwardedProps:{slots:w,slotProps:_,...x},ownerState:D,additionalProps:{type:"checkbox",icon:L.cloneElement(T,{fontSize:T.props.fontSize??p}),checkedIcon:L.cloneElement(I,{fontSize:I.props.fontSize??p}),disableRipple:m,slots:w,slotProps:{input:$zt(typeof M=="function"?M(D):M,{"data-indeterminate":c})}}});return k.jsx(O,{...F,classes:A})});function HPn(n){return n.substring(2).toLowerCase()}function O0r(n,e){return e.documentElement.clientWidth(setTimeout(()=>{d.current=!0},0),()=>{d.current=!1}),[]);const p=xm(jY(e),c),m=db(_=>{const x=h.current;h.current=!1;const T=fv(c.current);if(!d.current||!c.current||"clientX"in _&&O0r(_,T))return;if(l.current){l.current=!1;return}let I;_.composedPath?I=_.composedPath().includes(c.current):I=!T.documentElement.contains(_.target)||c.current.contains(_.target),!I&&(t||!x)&&r(_)}),b=_=>x=>{h.current=!0;const T=e.props[_];T&&T(x)},w={ref:p};return o!==!1&&(w[o]=b(o)),L.useEffect(()=>{if(o!==!1){const _=HPn(o),x=fv(c.current),T=()=>{l.current=!0};return x.addEventListener(_,m),x.addEventListener("touchmove",T),()=>{x.removeEventListener(_,m),x.removeEventListener("touchmove",T)}}},[m,o]),i!==!1&&(w[i]=b(i)),L.useEffect(()=>{if(i!==!1){const _=HPn(i),x=fv(c.current);return x.addEventListener(_,m),()=>{x.removeEventListener(_,m)}}},[m,i]),L.cloneElement(e,w)}const N0r={track:"#2b2b2b",thumb:"#6b6b6b",active:"#959595"};function P0r(n=N0r){return{scrollbarColor:`${n.thumb} ${n.track}`,"&::-webkit-scrollbar, & *::-webkit-scrollbar":{backgroundColor:n.track},"&::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb":{borderRadius:8,backgroundColor:n.thumb,minHeight:24,border:`3px solid ${n.track}`},"&::-webkit-scrollbar-thumb:focus, & *::-webkit-scrollbar-thumb:focus":{backgroundColor:n.active},"&::-webkit-scrollbar-thumb:active, & *::-webkit-scrollbar-thumb:active":{backgroundColor:n.active},"&::-webkit-scrollbar-thumb:hover, & *::-webkit-scrollbar-thumb:hover":{backgroundColor:n.active},"&::-webkit-scrollbar-corner, & *::-webkit-scrollbar-corner":{backgroundColor:n.track}}}function wui(n=window){const e=n.document.documentElement.clientWidth;return n.innerWidth-e}function F0r(n){const e=fv(n);return e.body===n?KL(n).innerWidth>e.documentElement.clientWidth:n.scrollHeight>n.clientHeight}function s4e(n,e){e?n.setAttribute("aria-hidden","true"):n.removeAttribute("aria-hidden")}function BPn(n){return parseFloat(KL(n).getComputedStyle(n).paddingRight)||0}function j0r(n){const t=["TEMPLATE","SCRIPT","STYLE","LINK","MAP","META","NOSCRIPT","PICTURE","COL","COLGROUP","PARAM","SLOT","SOURCE","TRACK"].includes(n.tagName),i=n.tagName==="INPUT"&&n.getAttribute("type")==="hidden";return t||i}function WPn(n,e,t,i,r){const o=[e,t,...i];[].forEach.call(n.children,l=>{const c=!o.includes(l),d=!j0r(l);c&&d&&s4e(l,r)})}function HEt(n,e){let t=-1;return n.some((i,r)=>e(i)?(t=r,!0):!1),t}function H0r(n,e){const t=[],i=n.container;if(!e.disableScrollLock){if(F0r(i)){const l=wui(KL(i));t.push({value:i.style.paddingRight,property:"padding-right",el:i}),i.style.paddingRight=`${BPn(i)+l}px`;const c=fv(i).querySelectorAll(".mui-fixed");[].forEach.call(c,d=>{t.push({value:d.style.paddingRight,property:"padding-right",el:d}),d.style.paddingRight=`${BPn(d)+l}px`})}let o;if(i.parentNode instanceof DocumentFragment)o=fv(i).body;else{const l=i.parentElement,c=KL(i);o=l?.nodeName==="HTML"&&c.getComputedStyle(l).overflowY==="scroll"?l:i}t.push({value:o.style.overflow,property:"overflow",el:o},{value:o.style.overflowX,property:"overflow-x",el:o},{value:o.style.overflowY,property:"overflow-y",el:o}),o.style.overflow="hidden"}return()=>{t.forEach(({value:o,el:l,property:c})=>{o?l.style.setProperty(c,o):l.style.removeProperty(c)})}}function B0r(n){const e=[];return[].forEach.call(n.children,t=>{t.getAttribute("aria-hidden")==="true"&&e.push(t)}),e}class W0r{constructor(){this.modals=[],this.containers=[]}add(e,t){let i=this.modals.indexOf(e);if(i!==-1)return i;i=this.modals.length,this.modals.push(e),e.modalRef&&s4e(e.modalRef,!1);const r=B0r(t);WPn(t,e.mount,e.modalRef,r,!0);const o=HEt(this.containers,l=>l.container===t);return o!==-1?(this.containers[o].modals.push(e),i):(this.containers.push({modals:[e],container:t,restore:null,hiddenSiblings:r}),i)}mount(e,t){const i=HEt(this.containers,o=>o.modals.includes(e)),r=this.containers[i];r.restore||(r.restore=H0r(r,t))}remove(e,t=!0){const i=this.modals.indexOf(e);if(i===-1)return i;const r=HEt(this.containers,l=>l.modals.includes(e)),o=this.containers[r];if(o.modals.splice(o.modals.indexOf(e),1),this.modals.splice(i,1),o.modals.length===0)o.restore&&o.restore(),e.modalRef&&s4e(e.modalRef,t),WPn(o.container,e.mount,e.modalRef,o.hiddenSiblings,!1),this.containers.splice(r,1);else{const l=o.modals[o.modals.length-1];l.modalRef&&s4e(l.modalRef,!1)}return i}isTopModal(e){return this.modals.length>0&&this.modals[this.modals.length-1]===e}}function Ffe(n){let e=n.activeElement;for(;e?.shadowRoot?.activeElement!=null;)e=e.shadowRoot.activeElement;return e}const V0r=["input","select","textarea","a[href]","button","[tabindex]","audio[controls]","video[controls]",'[contenteditable]:not([contenteditable="false"])'].join(",");function $0r(n){const e=parseInt(n.getAttribute("tabindex")||"",10);return Number.isNaN(e)?n.contentEditable==="true"||(n.nodeName==="AUDIO"||n.nodeName==="VIDEO"||n.nodeName==="DETAILS")&&n.getAttribute("tabindex")===null?0:n.tabIndex:e}function z0r(n){if(n.tagName!=="INPUT"||n.type!=="radio"||!n.name)return!1;const e=i=>n.ownerDocument.querySelector(`input[type="radio"]${i}`);let t=e(`[name="${n.name}"]:checked`);return t||(t=e(`[name="${n.name}"]`)),t!==n}function U0r(n){return!(n.disabled||n.tagName==="INPUT"&&n.type==="hidden"||z0r(n))}function q0r(n){const e=[],t=[];return Array.from(n.querySelectorAll(V0r)).forEach((i,r)=>{const o=$0r(i);o===-1||!U0r(i)||(o===0?e.push(i):t.push({documentOrder:r,tabIndex:o,node:i}))}),t.sort((i,r)=>i.tabIndex===r.tabIndex?i.documentOrder-r.documentOrder:i.tabIndex-r.tabIndex).map(i=>i.node).concat(e)}function G0r(){return!0}function yui(n){const{children:e,disableAutoFocus:t=!1,disableEnforceFocus:i=!1,disableRestoreFocus:r=!1,getTabbable:o=q0r,isEnabled:l=G0r,open:c}=n,d=L.useRef(!1),h=L.useRef(null),p=L.useRef(null),m=L.useRef(null),b=L.useRef(null),w=L.useRef(!1),_=L.useRef(null),x=xm(jY(e),_),T=L.useRef(null);L.useEffect(()=>{!c||!_.current||(w.current=!t)},[t,c]),L.useEffect(()=>{if(!c||!_.current)return;const A=fv(_.current),M=Ffe(A);return _.current.contains(M)||(_.current.hasAttribute("tabIndex")||_.current.setAttribute("tabIndex","-1"),w.current&&_.current.focus()),()=>{r||(m.current&&m.current.focus&&(d.current=!0,m.current.focus()),m.current=null)}},[c]),L.useEffect(()=>{if(!c||!_.current)return;const A=fv(_.current),M=j=>{if(T.current=j,i||!l()||j.key!=="Tab")return;Ffe(A)===_.current&&j.shiftKey&&(d.current=!0,p.current&&p.current.focus())},O=()=>{const j=_.current;if(j===null)return;const W=Ffe(A);if(!A.hasFocus()||!l()||d.current){d.current=!1;return}if(j.contains(W)||i&&W!==h.current&&W!==p.current)return;if(W!==b.current)b.current=null;else if(b.current!==null)return;if(!w.current)return;let U=[];if((W===h.current||W===p.current)&&(U=o(_.current)),U.length>0){const Z=!!(T.current?.shiftKey&&T.current?.key==="Tab"),te=U[0],G=U[U.length-1];typeof te!="string"&&typeof G!="string"&&(Z?G.focus():te.focus())}else j.focus()};A.addEventListener("focusin",O),A.addEventListener("keydown",M,!0);const F=setInterval(()=>{const j=Ffe(A);j&&j.tagName==="BODY"&&O()},50);return()=>{clearInterval(F),A.removeEventListener("focusin",O),A.removeEventListener("keydown",M,!0)}},[t,i,r,l,c,o]);const I=A=>{m.current===null&&(m.current=A.relatedTarget),w.current=!0,b.current=A.target;const M=e.props.onFocus;M&&M(A)},D=A=>{m.current===null&&(m.current=A.relatedTarget),w.current=!0};return k.jsxs(L.Fragment,{children:[k.jsx("div",{tabIndex:c?0:-1,onFocus:D,ref:h,"data-testid":"sentinelStart"}),L.cloneElement(e,{ref:x,onFocus:I}),k.jsx("div",{tabIndex:c?0:-1,onFocus:D,ref:p,"data-testid":"sentinelEnd"})]})}function K0r(n){return typeof n=="function"?n():n}function Y0r(n){return n?n.props.hasOwnProperty("in"):!1}const VPn=()=>{},NBe=new W0r;function Z0r(n){const{container:e,disableEscapeKeyDown:t=!1,disableScrollLock:i=!1,closeAfterTransition:r=!1,onTransitionEnter:o,onTransitionExited:l,children:c,onClose:d,open:h,rootRef:p}=n,m=L.useRef({}),b=L.useRef(null),w=L.useRef(null),_=xm(w,p),[x,T]=L.useState(!h),I=Y0r(c);let D=!0;(n["aria-hidden"]==="false"||n["aria-hidden"]===!1)&&(D=!1);const A=()=>fv(b.current),M=()=>(m.current.modalRef=w.current,m.current.mount=b.current,m.current),O=()=>{NBe.mount(M(),{disableScrollLock:i}),w.current&&(w.current.scrollTop=0)},F=db(()=>{const ie=K0r(e)||A().body;NBe.add(M(),ie),w.current&&O()}),j=()=>NBe.isTopModal(M()),W=db(ie=>{b.current=ie,ie&&(h&&j()?O():w.current&&s4e(w.current,D))}),U=L.useCallback(()=>{NBe.remove(M(),D)},[D]);L.useEffect(()=>()=>{U()},[U]),L.useEffect(()=>{h?F():(!I||!r)&&U()},[h,U,I,r,F]);const Z=ie=>se=>{ie.onKeyDown?.(se),!(se.key!=="Escape"||se.which===229||!j())&&(t||(se.stopPropagation(),d&&d(se,"escapeKeyDown")))},te=ie=>se=>{ie.onClick?.(se),se.target===se.currentTarget&&d&&d(se,"backdropClick")};return{getRootProps:(ie={})=>{const se=bie(n);delete se.onTransitionEnter,delete se.onTransitionExited;const ue={...se,...ie};return{role:"presentation",...ue,onKeyDown:Z(ue),ref:_}},getBackdropProps:(ie={})=>{const se=ie;return{"aria-hidden":!0,...se,onClick:te(se),open:h}},getTransitionProps:()=>{const ie=()=>{T(!1),o&&o()},se=()=>{T(!0),l&&l(),r&&U()};return{onEnter:$Ot(ie,c?.props.onEnter??VPn),onExited:$Ot(se,c?.props.onExited??VPn)}},rootRef:_,portalRef:W,isTopModal:j,exited:x,hasTransition:I}}function X0r(n){return Po("MuiModal",n)}Fo("MuiModal",["root","hidden","backdrop"]);const Q0r=n=>{const{open:e,exited:t,classes:i}=n;return jo({root:["root",!e&&t&&"hidden"],backdrop:["backdrop"]},X0r,i)},J0r=nn("div",{name:"MuiModal",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,!t.open&&t.exited&&e.hidden]}})(Gs(({theme:n})=>({position:"fixed",zIndex:(n.vars||n).zIndex.modal,right:0,bottom:0,top:0,left:0,variants:[{props:({ownerState:e})=>!e.open&&e.exited,style:{visibility:"hidden"}}]}))),ebr=nn(oUt,{name:"MuiModal",slot:"Backdrop"})({zIndex:-1}),Zet=L.forwardRef(function(e,t){const i=Vo({name:"MuiModal",props:e}),{BackdropComponent:r=ebr,BackdropProps:o,classes:l,className:c,closeAfterTransition:d=!1,children:h,container:p,component:m,components:b={},componentsProps:w={},disableAutoFocus:_=!1,disableEnforceFocus:x=!1,disableEscapeKeyDown:T=!1,disablePortal:I=!1,disableRestoreFocus:D=!1,disableScrollLock:A=!1,hideBackdrop:M=!1,keepMounted:O=!1,onClose:F,onTransitionEnter:j,onTransitionExited:W,open:U,slotProps:Z={},slots:te={},theme:G,...ee}=i,Q={...i,closeAfterTransition:d,disableAutoFocus:_,disableEnforceFocus:x,disableEscapeKeyDown:T,disablePortal:I,disableRestoreFocus:D,disableScrollLock:A,hideBackdrop:M,keepMounted:O},{getRootProps:ie,getBackdropProps:se,getTransitionProps:ue,portalRef:ne,isTopModal:we,exited:de,hasTransition:ce}=Z0r({...Q,rootRef:t}),ye={...Q,exited:de},he=Q0r(ye),pe={};if(h.props.tabIndex===void 0&&(pe.tabIndex="-1"),ce){const{onEnter:et,onExited:Ge}=ue();pe.onEnter=et,pe.onExited=Ge}const me={slots:{root:b.Root,backdrop:b.Backdrop,...te},slotProps:{...w,...Z}},[be,xe]=_o("root",{ref:t,elementType:J0r,externalForwardedProps:{...me,...ee,component:m},getSlotProps:ie,ownerState:ye,className:_i(c,he?.root,!ye.open&&ye.exited&&he?.hidden)}),[Te,qe]=_o("backdrop",{ref:o?.ref,elementType:r,externalForwardedProps:me,shouldForwardComponentProp:!0,additionalProps:o,getSlotProps:et=>se({...et,onClick:Ge=>{et?.onClick&&et.onClick(Ge)}}),className:_i(o?.className,he?.backdrop),ownerState:ye});return!O&&!U&&(!ce||de)?null:k.jsx(fui,{ref:ne,container:p,disablePortal:I,children:k.jsxs(be,{...xe,children:[!M&&r?k.jsx(Te,{...qe}):null,k.jsx(yui,{disableEnforceFocus:x,disableAutoFocus:_,disableRestoreFocus:D,isEnabled:we,open:U,children:L.cloneElement(h,pe)})]})})});function tbr(n){return Po("MuiDialog",n)}const o4e=Fo("MuiDialog",["root","backdrop","scrollPaper","scrollBody","container","paper","paperScrollPaper","paperScrollBody","paperWidthFalse","paperWidthXs","paperWidthSm","paperWidthMd","paperWidthLg","paperWidthXl","paperFullWidth","paperFullScreen"]),_ui=L.createContext({}),nbr=nn(oUt,{name:"MuiDialog",slot:"Backdrop"})({zIndex:-1}),ibr=n=>{const{classes:e,scroll:t,maxWidth:i,fullWidth:r,fullScreen:o}=n,l={root:["root"],backdrop:["backdrop"],container:["container",`scroll${ri(t)}`],paper:["paper",`paperScroll${ri(t)}`,`paperWidth${ri(String(i))}`,r&&"paperFullWidth",o&&"paperFullScreen"]};return jo(l,tbr,e)},rbr=nn(Zet,{name:"MuiDialog",slot:"Root"})({"@media print":{position:"absolute !important"}}),sbr=nn("div",{name:"MuiDialog",slot:"Container",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.container,e[`scroll${ri(t.scroll)}`]]}})({height:"100%","@media print":{height:"auto"},outline:0,variants:[{props:{scroll:"paper"},style:{display:"flex",justifyContent:"center",alignItems:"center"}},{props:{scroll:"body"},style:{overflowY:"auto",overflowX:"hidden",textAlign:"center","&::after":{content:'""',display:"inline-block",verticalAlign:"middle",height:"100%",width:"0"}}}]}),obr=nn(Qf,{name:"MuiDialog",slot:"Paper",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.paper,e[`scrollPaper${ri(t.scroll)}`],e[`paperWidth${ri(String(t.maxWidth))}`],t.fullWidth&&e.paperFullWidth,t.fullScreen&&e.paperFullScreen]}})(Gs(({theme:n})=>({margin:32,position:"relative",overflowY:"auto","@media print":{overflowY:"visible",boxShadow:"none"},variants:[{props:{scroll:"paper"},style:{display:"flex",flexDirection:"column",maxHeight:"calc(100% - 64px)"}},{props:{scroll:"body"},style:{display:"inline-block",verticalAlign:"middle",textAlign:"initial"}},{props:({ownerState:e})=>!e.maxWidth,style:{maxWidth:"calc(100% - 64px)"}},{props:{maxWidth:"xs"},style:{maxWidth:n.breakpoints.unit==="px"?Math.max(n.breakpoints.values.xs,444):`max(${n.breakpoints.values.xs}${n.breakpoints.unit}, 444px)`,[`&.${o4e.paperScrollBody}`]:{[n.breakpoints.down(Math.max(n.breakpoints.values.xs,444)+64)]:{maxWidth:"calc(100% - 64px)"}}}},...Object.keys(n.breakpoints.values).filter(e=>e!=="xs").map(e=>({props:{maxWidth:e},style:{maxWidth:`${n.breakpoints.values[e]}${n.breakpoints.unit}`,[`&.${o4e.paperScrollBody}`]:{[n.breakpoints.down(n.breakpoints.values[e]+64)]:{maxWidth:"calc(100% - 64px)"}}}})),{props:({ownerState:e})=>e.fullWidth,style:{width:"calc(100% - 64px)"}},{props:({ownerState:e})=>e.fullScreen,style:{margin:0,width:"100%",maxWidth:"100%",height:"100%",maxHeight:"none",borderRadius:0,[`&.${o4e.paperScrollBody}`]:{margin:0,maxWidth:"100%"}}}]}))),l8=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiDialog"}),r=Tf(),o={enter:r.transitions.duration.enteringScreen,exit:r.transitions.duration.leavingScreen},{"aria-describedby":l,"aria-labelledby":c,"aria-modal":d=!0,BackdropComponent:h,BackdropProps:p,children:m,className:b,disableEscapeKeyDown:w=!1,fullScreen:_=!1,fullWidth:x=!1,maxWidth:T="sm",onClick:I,onClose:D,open:A,PaperComponent:M=Qf,PaperProps:O={},scroll:F="paper",slots:j={},slotProps:W={},TransitionComponent:U=tY,transitionDuration:Z=o,TransitionProps:te,...G}=i,ee={...i,disableEscapeKeyDown:w,fullScreen:_,fullWidth:x,maxWidth:T,scroll:F},Q=ibr(ee),ie=L.useRef(),se=He=>{ie.current=He.target===He.currentTarget},ue=He=>{I&&I(He),ie.current&&(ie.current=null,D&&D(He,"backdropClick"))},ne=KW(c),we=L.useMemo(()=>({titleId:ne}),[ne]),de={transition:U,...j},ce={transition:te,paper:O,backdrop:p,...W},ye={slots:de,slotProps:ce},[he,pe]=_o("root",{elementType:rbr,shouldForwardComponentProp:!0,externalForwardedProps:ye,ownerState:ee,className:_i(Q.root,b),ref:t}),[me,be]=_o("backdrop",{elementType:nbr,shouldForwardComponentProp:!0,externalForwardedProps:ye,ownerState:ee,className:Q.backdrop}),[xe,Te]=_o("paper",{elementType:obr,shouldForwardComponentProp:!0,externalForwardedProps:ye,ownerState:ee,className:_i(Q.paper,O.className)}),[qe,et]=_o("container",{elementType:sbr,externalForwardedProps:ye,ownerState:ee,className:Q.container}),[Ge,Me]=_o("transition",{elementType:tY,externalForwardedProps:ye,ownerState:ee,additionalProps:{appear:!0,in:A,timeout:Z,role:"presentation"}});return k.jsx(he,{closeAfterTransition:!0,slots:{backdrop:me},slotProps:{backdrop:{transitionDuration:Z,as:h,...be}},disableEscapeKeyDown:w,onClose:D,open:A,onClick:ue,...pe,...G,children:k.jsx(Ge,{...Me,children:k.jsx(qe,{onMouseDown:se,...et,children:k.jsx(xe,{as:M,elevation:24,role:"dialog","aria-describedby":l,"aria-labelledby":ne,"aria-modal":d,...Te,children:k.jsx(_ui.Provider,{value:we,children:m})})})})})});function abr(n){return Po("MuiDialogActions",n)}Fo("MuiDialogActions",["root","spacing"]);const lbr=n=>{const{classes:e,disableSpacing:t}=n;return jo({root:["root",!t&&"spacing"]},abr,e)},cbr=nn("div",{name:"MuiDialogActions",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,!t.disableSpacing&&e.spacing]}})({display:"flex",alignItems:"center",padding:8,justifyContent:"flex-end",flex:"0 0 auto",variants:[{props:({ownerState:n})=>!n.disableSpacing,style:{"& > :not(style) ~ :not(style)":{marginLeft:8}}}]}),$3=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiDialogActions"}),{className:r,disableSpacing:o=!1,...l}=i,c={...i,disableSpacing:o},d=lbr(c);return k.jsx(cbr,{className:_i(d.root,r),ownerState:c,ref:t,...l})});function ubr(n){return Po("MuiDialogContent",n)}Fo("MuiDialogContent",["root","dividers"]);function dbr(n){return Po("MuiDialogTitle",n)}const hbr=Fo("MuiDialogTitle",["root"]),fbr=n=>{const{classes:e,dividers:t}=n;return jo({root:["root",t&&"dividers"]},ubr,e)},pbr=nn("div",{name:"MuiDialogContent",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.dividers&&e.dividers]}})(Gs(({theme:n})=>({flex:"1 1 auto",WebkitOverflowScrolling:"touch",overflowY:"auto",padding:"20px 24px",variants:[{props:({ownerState:e})=>e.dividers,style:{padding:"16px 24px",borderTop:`1px solid ${(n.vars||n).palette.divider}`,borderBottom:`1px solid ${(n.vars||n).palette.divider}`}},{props:({ownerState:e})=>!e.dividers,style:{[`.${hbr.root} + &`]:{paddingTop:0}}}]}))),z3=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiDialogContent"}),{className:r,dividers:o=!1,...l}=i,c={...i,dividers:o},d=fbr(c);return k.jsx(pbr,{className:_i(d.root,r),ownerState:c,ref:t,...l})}),gbr=n=>{const{classes:e}=n;return jo({root:["root"]},dbr,e)},mbr=nn(ei,{name:"MuiDialogTitle",slot:"Root"})({padding:"16px 24px",flex:"0 0 auto"}),IM=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiDialogTitle"}),{className:r,id:o,...l}=i,c=i,d=gbr(c),{titleId:h=o}=L.useContext(_ui);return k.jsx(mbr,{component:"h2",className:_i(d.root,r),ownerState:c,ref:t,variant:"h6",id:o??h,...l})});function bbr(n){return Po("MuiDivider",n)}const $Pn=Fo("MuiDivider",["root","absolute","fullWidth","inset","middle","flexItem","light","vertical","withChildren","withChildrenVertical","textAlignRight","textAlignLeft","wrapper","wrapperVertical"]),vbr=n=>{const{absolute:e,children:t,classes:i,flexItem:r,light:o,orientation:l,textAlign:c,variant:d}=n;return jo({root:["root",e&&"absolute",d,o&&"light",l==="vertical"&&"vertical",r&&"flexItem",t&&"withChildren",t&&l==="vertical"&&"withChildrenVertical",c==="right"&&l!=="vertical"&&"textAlignRight",c==="left"&&l!=="vertical"&&"textAlignLeft"],wrapper:["wrapper",l==="vertical"&&"wrapperVertical"]},bbr,i)},wbr=nn("div",{name:"MuiDivider",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.absolute&&e.absolute,e[t.variant],t.light&&e.light,t.orientation==="vertical"&&e.vertical,t.flexItem&&e.flexItem,t.children&&e.withChildren,t.children&&t.orientation==="vertical"&&e.withChildrenVertical,t.textAlign==="right"&&t.orientation!=="vertical"&&e.textAlignRight,t.textAlign==="left"&&t.orientation!=="vertical"&&e.textAlignLeft]}})(Gs(({theme:n})=>({margin:0,flexShrink:0,borderWidth:0,borderStyle:"solid",borderColor:(n.vars||n).palette.divider,borderBottomWidth:"thin",variants:[{props:{absolute:!0},style:{position:"absolute",bottom:0,left:0,width:"100%"}},{props:{light:!0},style:{borderColor:n.alpha((n.vars||n).palette.divider,.08)}},{props:{variant:"inset"},style:{marginLeft:72}},{props:{variant:"middle",orientation:"horizontal"},style:{marginLeft:n.spacing(2),marginRight:n.spacing(2)}},{props:{variant:"middle",orientation:"vertical"},style:{marginTop:n.spacing(1),marginBottom:n.spacing(1)}},{props:{orientation:"vertical"},style:{height:"100%",borderBottomWidth:0,borderRightWidth:"thin"}},{props:{flexItem:!0},style:{alignSelf:"stretch",height:"auto"}},{props:({ownerState:e})=>!!e.children,style:{display:"flex",textAlign:"center",border:0,borderTopStyle:"solid",borderLeftStyle:"solid","&::before, &::after":{content:'""',alignSelf:"center"}}},{props:({ownerState:e})=>e.children&&e.orientation!=="vertical",style:{"&::before, &::after":{width:"100%",borderTop:`thin solid ${(n.vars||n).palette.divider}`,borderTopStyle:"inherit"}}},{props:({ownerState:e})=>e.orientation==="vertical"&&e.children,style:{flexDirection:"column","&::before, &::after":{height:"100%",borderLeft:`thin solid ${(n.vars||n).palette.divider}`,borderLeftStyle:"inherit"}}},{props:({ownerState:e})=>e.textAlign==="right"&&e.orientation!=="vertical",style:{"&::before":{width:"90%"},"&::after":{width:"10%"}}},{props:({ownerState:e})=>e.textAlign==="left"&&e.orientation!=="vertical",style:{"&::before":{width:"10%"},"&::after":{width:"90%"}}}]}))),ybr=nn("span",{name:"MuiDivider",slot:"Wrapper",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.wrapper,t.orientation==="vertical"&&e.wrapperVertical]}})(Gs(({theme:n})=>({display:"inline-block",paddingLeft:`calc(${n.spacing(1)} * 1.2)`,paddingRight:`calc(${n.spacing(1)} * 1.2)`,whiteSpace:"nowrap",variants:[{props:{orientation:"vertical"},style:{paddingTop:`calc(${n.spacing(1)} * 1.2)`,paddingBottom:`calc(${n.spacing(1)} * 1.2)`}}]}))),nY=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiDivider"}),{absolute:r=!1,children:o,className:l,orientation:c="horizontal",component:d=o||c==="vertical"?"div":"hr",flexItem:h=!1,light:p=!1,role:m=d!=="hr"?"separator":void 0,textAlign:b="center",variant:w="fullWidth",..._}=i,x={...i,absolute:r,component:d,flexItem:h,light:p,orientation:c,role:m,textAlign:b,variant:w},T=vbr(x);return k.jsx(wbr,{as:d,className:_i(T.root,l),role:m,ref:t,ownerState:x,"aria-orientation":m==="separator"&&(d!=="hr"||c==="vertical")?c:void 0,..._,children:o?k.jsx(ybr,{className:T.wrapper,ownerState:x,children:o}):null})});nY&&(nY.muiSkipListHighlight=!0);function _br(n,e,t){const i=e.getBoundingClientRect(),r=t&&t.getBoundingClientRect(),o=KL(e);let l;if(e.fakeTransform)l=e.fakeTransform;else{const h=o.getComputedStyle(e);l=h.getPropertyValue("-webkit-transform")||h.getPropertyValue("transform")}let c=0,d=0;if(l&&l!=="none"&&typeof l=="string"){const h=l.split("(")[1].split(")")[0].split(",");c=parseInt(h[4],10),d=parseInt(h[5],10)}return n==="left"?r?`translateX(${r.right+c-i.left}px)`:`translateX(${o.innerWidth+c-i.left}px)`:n==="right"?r?`translateX(-${i.right-r.left-c}px)`:`translateX(-${i.left+i.width-c}px)`:n==="up"?r?`translateY(${r.bottom+d-i.top}px)`:`translateY(${o.innerHeight+d-i.top}px)`:r?`translateY(-${i.top-r.top+i.height-d}px)`:`translateY(-${i.top+i.height-d}px)`}function Cbr(n){return typeof n=="function"?n():n}function PBe(n,e,t){const i=Cbr(t),r=_br(n,e,i);r&&(e.style.webkitTransform=r,e.style.transform=r)}const Sbr=L.forwardRef(function(e,t){const i=Tf(),r={enter:i.transitions.easing.easeOut,exit:i.transitions.easing.sharp},o={enter:i.transitions.duration.enteringScreen,exit:i.transitions.duration.leavingScreen},{addEndListener:l,appear:c=!0,children:d,container:h,direction:p="down",easing:m=r,in:b,onEnter:w,onEntered:_,onEntering:x,onExit:T,onExited:I,onExiting:D,style:A,timeout:M=o,TransitionComponent:O=o8,...F}=e,j=L.useRef(null),W=xm(jY(d),j,t),U=ne=>we=>{ne&&(we===void 0?ne(j.current):ne(j.current,we))},Z=U((ne,we)=>{PBe(p,ne,h),Gzt(ne),w&&w(ne,we)}),te=U((ne,we)=>{const de=JK({timeout:M,style:A,easing:m},{mode:"enter"});ne.style.webkitTransition=i.transitions.create("-webkit-transform",{...de}),ne.style.transition=i.transitions.create("transform",{...de}),ne.style.webkitTransform="none",ne.style.transform="none",x&&x(ne,we)}),G=U(_),ee=U(D),Q=U(ne=>{const we=JK({timeout:M,style:A,easing:m},{mode:"exit"});ne.style.webkitTransition=i.transitions.create("-webkit-transform",we),ne.style.transition=i.transitions.create("transform",we),PBe(p,ne,h),T&&T(ne)}),ie=U(ne=>{ne.style.webkitTransition="",ne.style.transition="",I&&I(ne)}),se=ne=>{l&&l(j.current,ne)},ue=L.useCallback(()=>{j.current&&PBe(p,j.current,h)},[p,h]);return L.useEffect(()=>{if(b||p==="down"||p==="right")return;const ne=p5e(()=>{j.current&&PBe(p,j.current,h)}),we=KL(j.current);return we.addEventListener("resize",ne),()=>{ne.clear(),we.removeEventListener("resize",ne)}},[p,b,h]),L.useEffect(()=>{b||ue()},[b,ue]),k.jsx(O,{nodeRef:j,onEnter:Z,onEntered:G,onEntering:te,onExit:Q,onExited:ie,onExiting:ee,addEndListener:se,appear:c,in:b,timeout:M,...F,children:(ne,{ownerState:we,...de})=>L.cloneElement(d,{ref:W,style:{visibility:ne==="exited"&&!b?"hidden":void 0,...A,...d.props.style},...de})})});function xbr(n){return Po("MuiDrawer",n)}Fo("MuiDrawer",["root","docked","paper","anchorLeft","anchorRight","anchorTop","anchorBottom","paperAnchorLeft","paperAnchorRight","paperAnchorTop","paperAnchorBottom","paperAnchorDockedLeft","paperAnchorDockedRight","paperAnchorDockedTop","paperAnchorDockedBottom","modal"]);const Cui=(n,e)=>{const{ownerState:t}=n;return[e.root,(t.variant==="permanent"||t.variant==="persistent")&&e.docked,t.variant==="temporary"&&e.modal]},Ebr=n=>{const{classes:e,anchor:t,variant:i}=n,r={root:["root",`anchor${ri(t)}`],docked:[(i==="permanent"||i==="persistent")&&"docked"],modal:["modal"],paper:["paper",`paperAnchor${ri(t)}`,i!=="temporary"&&`paperAnchorDocked${ri(t)}`]};return jo(r,xbr,e)},kbr=nn(Zet,{name:"MuiDrawer",slot:"Root",overridesResolver:Cui})(Gs(({theme:n})=>({zIndex:(n.vars||n).zIndex.drawer}))),Tbr=nn("div",{shouldForwardProp:$_,name:"MuiDrawer",slot:"Docked",skipVariantsResolver:!1,overridesResolver:Cui})({flex:"0 0 auto"}),Lbr=nn(Qf,{name:"MuiDrawer",slot:"Paper",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.paper,e[`paperAnchor${ri(t.anchor)}`],t.variant!=="temporary"&&e[`paperAnchorDocked${ri(t.anchor)}`]]}})(Gs(({theme:n})=>({overflowY:"auto",display:"flex",flexDirection:"column",height:"100%",flex:"1 0 auto",zIndex:(n.vars||n).zIndex.drawer,WebkitOverflowScrolling:"touch",position:"fixed",top:0,outline:0,variants:[{props:{anchor:"left"},style:{left:0}},{props:{anchor:"top"},style:{top:0,left:0,right:0,height:"auto",maxHeight:"100%"}},{props:{anchor:"right"},style:{right:0}},{props:{anchor:"bottom"},style:{top:"auto",left:0,bottom:0,right:0,height:"auto",maxHeight:"100%"}},{props:({ownerState:e})=>e.anchor==="left"&&e.variant!=="temporary",style:{borderRight:`1px solid ${(n.vars||n).palette.divider}`}},{props:({ownerState:e})=>e.anchor==="top"&&e.variant!=="temporary",style:{borderBottom:`1px solid ${(n.vars||n).palette.divider}`}},{props:({ownerState:e})=>e.anchor==="right"&&e.variant!=="temporary",style:{borderLeft:`1px solid ${(n.vars||n).palette.divider}`}},{props:({ownerState:e})=>e.anchor==="bottom"&&e.variant!=="temporary",style:{borderTop:`1px solid ${(n.vars||n).palette.divider}`}}]}))),Sui={left:"right",right:"left",top:"down",bottom:"up"};function Dbr(n){return["left","right"].includes(n)}function Ibr({direction:n},e){return n==="rtl"&&Dbr(e)?Sui[e]:e}const xui=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiDrawer"}),r=Tf(),o=OY(),l={enter:r.transitions.duration.enteringScreen,exit:r.transitions.duration.leavingScreen},{anchor:c="left",BackdropProps:d,children:h,className:p,elevation:m=16,hideBackdrop:b=!1,ModalProps:{BackdropProps:w,..._}={},onClose:x,open:T=!1,PaperProps:I={},SlideProps:D,TransitionComponent:A,transitionDuration:M=l,variant:O="temporary",slots:F={},slotProps:j={},...W}=i,U=L.useRef(!1);L.useEffect(()=>{U.current=!0},[]);const Z=Ibr({direction:o?"rtl":"ltr"},c),G={...i,anchor:c,elevation:m,open:T,variant:O,...W},ee=Ebr(G),Q={slots:{transition:A,...F},slotProps:{paper:I,transition:D,...j,backdrop:$zt(j.backdrop||{...d,...w},{transitionDuration:M})}},[ie,se]=_o("root",{ref:t,elementType:kbr,className:_i(ee.root,ee.modal,p),shouldForwardComponentProp:!0,ownerState:G,externalForwardedProps:{...Q,...W,..._},additionalProps:{open:T,onClose:x,hideBackdrop:b,slots:{backdrop:Q.slots.backdrop},slotProps:{backdrop:Q.slotProps.backdrop}}}),[ue,ne]=_o("paper",{elementType:Lbr,shouldForwardComponentProp:!0,className:_i(ee.paper,I.className),ownerState:G,externalForwardedProps:Q,additionalProps:{elevation:O==="temporary"?m:0,square:!0,...O==="temporary"&&{role:"dialog","aria-modal":"true"}}}),[we,de]=_o("docked",{elementType:Tbr,ref:t,className:_i(ee.root,ee.docked,p),ownerState:G,externalForwardedProps:Q,additionalProps:W}),[ce,ye]=_o("transition",{elementType:Sbr,ownerState:G,externalForwardedProps:Q,additionalProps:{in:T,direction:Sui[Z],timeout:M,appear:U.current}}),he=k.jsx(ue,{...ne,children:h});if(O==="permanent")return k.jsx(we,{...de,children:he});const pe=k.jsx(ce,{...ye,children:he});return O==="persistent"?k.jsx(we,{...de,children:pe}):k.jsx(ie,{...se,children:pe})}),Abr=n=>{const{classes:e,disableUnderline:t,startAdornment:i,endAdornment:r,size:o,hiddenLabel:l,multiline:c}=n,d={root:["root",!t&&"underline",i&&"adornedStart",r&&"adornedEnd",o==="small"&&`size${ri(o)}`,l&&"hiddenLabel",c&&"multiline"],input:["input"]},h=jo(d,b1r,e);return{...e,...h}},Rbr=nn(Get,{shouldForwardProp:n=>$_(n)||n==="classes",name:"MuiFilledInput",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[...Uet(n,e),!t.disableUnderline&&e.underline]}})(Gs(({theme:n})=>{const e=n.palette.mode==="light",t=e?"rgba(0, 0, 0, 0.42)":"rgba(255, 255, 255, 0.7)",i=e?"rgba(0, 0, 0, 0.06)":"rgba(255, 255, 255, 0.09)",r=e?"rgba(0, 0, 0, 0.09)":"rgba(255, 255, 255, 0.13)",o=e?"rgba(0, 0, 0, 0.12)":"rgba(255, 255, 255, 0.12)";return{position:"relative",backgroundColor:n.vars?n.vars.palette.FilledInput.bg:i,borderTopLeftRadius:(n.vars||n).shape.borderRadius,borderTopRightRadius:(n.vars||n).shape.borderRadius,transition:n.transitions.create("background-color",{duration:n.transitions.duration.shorter,easing:n.transitions.easing.easeOut}),"&:hover":{backgroundColor:n.vars?n.vars.palette.FilledInput.hoverBg:r,"@media (hover: none)":{backgroundColor:n.vars?n.vars.palette.FilledInput.bg:i}},[`&.${bL.focused}`]:{backgroundColor:n.vars?n.vars.palette.FilledInput.bg:i},[`&.${bL.disabled}`]:{backgroundColor:n.vars?n.vars.palette.FilledInput.disabledBg:o},variants:[{props:({ownerState:l})=>!l.disableUnderline,style:{"&::after":{left:0,bottom:0,content:'""',position:"absolute",right:0,transform:"scaleX(0)",transition:n.transitions.create("transform",{duration:n.transitions.duration.shorter,easing:n.transitions.easing.easeOut}),pointerEvents:"none"},[`&.${bL.focused}:after`]:{transform:"scaleX(1) translateX(0)"},[`&.${bL.error}`]:{"&::before, &::after":{borderBottomColor:(n.vars||n).palette.error.main}},"&::before":{borderBottom:`1px solid ${n.vars?n.alpha(n.vars.palette.common.onBackground,n.vars.opacity.inputUnderline):t}`,left:0,bottom:0,content:'"\\00a0"',position:"absolute",right:0,transition:n.transitions.create("border-bottom-color",{duration:n.transitions.duration.shorter}),pointerEvents:"none"},[`&:hover:not(.${bL.disabled}, .${bL.error}):before`]:{borderBottom:`1px solid ${(n.vars||n).palette.text.primary}`},[`&.${bL.disabled}:before`]:{borderBottomStyle:"dotted"}}},...Object.entries(n.palette).filter(Vh()).map(([l])=>({props:{disableUnderline:!1,color:l},style:{"&::after":{borderBottom:`2px solid ${(n.vars||n).palette[l]?.main}`}}})),{props:({ownerState:l})=>l.startAdornment,style:{paddingLeft:12}},{props:({ownerState:l})=>l.endAdornment,style:{paddingRight:12}},{props:({ownerState:l})=>l.multiline,style:{padding:"25px 12px 8px"}},{props:({ownerState:l,size:c})=>l.multiline&&c==="small",style:{paddingTop:21,paddingBottom:4}},{props:({ownerState:l})=>l.multiline&&l.hiddenLabel,style:{paddingTop:16,paddingBottom:17}},{props:({ownerState:l})=>l.multiline&&l.hiddenLabel&&l.size==="small",style:{paddingTop:8,paddingBottom:9}}]}})),Mbr=nn(Ket,{name:"MuiFilledInput",slot:"Input",overridesResolver:qet})(Gs(({theme:n})=>({paddingTop:25,paddingRight:12,paddingBottom:8,paddingLeft:12,...!n.vars&&{"&:-webkit-autofill":{WebkitBoxShadow:n.palette.mode==="light"?null:"0 0 0 100px #266798 inset",WebkitTextFillColor:n.palette.mode==="light"?null:"#fff",caretColor:n.palette.mode==="light"?null:"#fff",borderTopLeftRadius:"inherit",borderTopRightRadius:"inherit"}},...n.vars&&{"&:-webkit-autofill":{borderTopLeftRadius:"inherit",borderTopRightRadius:"inherit"},[n.getColorSchemeSelector("dark")]:{"&:-webkit-autofill":{WebkitBoxShadow:"0 0 0 100px #266798 inset",WebkitTextFillColor:"#fff",caretColor:"#fff"}}},variants:[{props:{size:"small"},style:{paddingTop:21,paddingBottom:4}},{props:({ownerState:e})=>e.hiddenLabel,style:{paddingTop:16,paddingBottom:17}},{props:({ownerState:e})=>e.startAdornment,style:{paddingLeft:0}},{props:({ownerState:e})=>e.endAdornment,style:{paddingRight:0}},{props:({ownerState:e})=>e.hiddenLabel&&e.size==="small",style:{paddingTop:8,paddingBottom:9}},{props:({ownerState:e})=>e.multiline,style:{paddingTop:0,paddingBottom:0,paddingLeft:0,paddingRight:0}}]}))),lUt=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiFilledInput"}),{disableUnderline:r=!1,components:o={},componentsProps:l,fullWidth:c=!1,hiddenLabel:d,inputComponent:h="input",multiline:p=!1,slotProps:m,slots:b={},type:w="text",..._}=i,x={...i,disableUnderline:r,fullWidth:c,inputComponent:h,multiline:p,type:w},T=Abr(i),I={root:{ownerState:x},input:{ownerState:x}},D=m??l?F_(I,m??l):I,A=b.root??o.Root??Rbr,M=b.input??o.Input??Mbr;return k.jsx(U1e,{slots:{root:A,input:M},slotProps:D,fullWidth:c,inputComponent:h,multiline:p,ref:t,type:w,..._,classes:T})});lUt.muiName="Input";function Obr(n){return Po("MuiFormControl",n)}Fo("MuiFormControl",["root","marginNone","marginNormal","marginDense","fullWidth","disabled"]);const Nbr=n=>{const{classes:e,margin:t,fullWidth:i}=n,r={root:["root",t!=="none"&&`margin${ri(t)}`,i&&"fullWidth"]};return jo(r,Obr,e)},Pbr=nn("div",{name:"MuiFormControl",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[`margin${ri(t.margin)}`],t.fullWidth&&e.fullWidth]}})({display:"inline-flex",flexDirection:"column",position:"relative",minWidth:0,padding:0,margin:0,border:0,verticalAlign:"top",variants:[{props:{margin:"normal"},style:{marginTop:16,marginBottom:8}},{props:{margin:"dense"},style:{marginTop:8,marginBottom:4}},{props:{fullWidth:!0},style:{width:"100%"}}]}),vW=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiFormControl"}),{children:r,className:o,color:l="primary",component:c="div",disabled:d=!1,error:h=!1,focused:p,fullWidth:m=!1,hiddenLabel:b=!1,margin:w="none",required:_=!1,size:x="medium",variant:T="outlined",...I}=i,D={...i,color:l,component:c,disabled:d,error:h,fullWidth:m,hiddenLabel:b,margin:w,required:_,size:x,variant:T},A=Nbr(D),[M,O]=L.useState(()=>{let ie=!1;return r&&L.Children.forEach(r,se=>{if(!e4e(se,["Input","Select"]))return;const ue=e4e(se,["Select"])?se.props.input:se;ue&&h1r(ue.props)&&(ie=!0)}),ie}),[F,j]=L.useState(()=>{let ie=!1;return r&&L.Children.forEach(r,se=>{e4e(se,["Input","Select"])&&(oGe(se.props,!0)||oGe(se.props.inputProps,!0))&&(ie=!0)}),ie}),[W,U]=L.useState(!1);d&&W&&U(!1);const Z=p!==void 0&&!d?p:W;let te;L.useRef(!1);const G=L.useCallback(()=>{j(!0)},[]),ee=L.useCallback(()=>{j(!1)},[]),Q=L.useMemo(()=>({adornedStart:M,setAdornedStart:O,color:l,disabled:d,error:h,filled:F,focused:Z,fullWidth:m,hiddenLabel:b,size:x,onBlur:()=>{U(!1)},onFocus:()=>{U(!0)},onEmpty:ee,onFilled:G,registerEffect:te,required:_,variant:T}),[M,l,d,h,F,Z,m,b,te,ee,G,_,x,T]);return k.jsx(zet.Provider,{value:Q,children:k.jsx(Pbr,{as:c,ownerState:D,className:_i(A.root,o),ref:t,...I,children:r})})});function Fbr(n){return Po("MuiFormControlLabel",n)}const hke=Fo("MuiFormControlLabel",["root","labelPlacementStart","labelPlacementTop","labelPlacementBottom","disabled","label","error","required","asterisk"]),jbr=n=>{const{classes:e,disabled:t,labelPlacement:i,error:r,required:o}=n,l={root:["root",t&&"disabled",`labelPlacement${ri(i)}`,r&&"error",o&&"required"],label:["label",t&&"disabled"],asterisk:["asterisk",r&&"error"]};return jo(l,Fbr,e)},Hbr=nn("label",{name:"MuiFormControlLabel",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[{[`& .${hke.label}`]:e.label},e.root,e[`labelPlacement${ri(t.labelPlacement)}`]]}})(Gs(({theme:n})=>({display:"inline-flex",alignItems:"center",cursor:"pointer",verticalAlign:"middle",WebkitTapHighlightColor:"transparent",marginLeft:-11,marginRight:16,[`&.${hke.disabled}`]:{cursor:"default"},[`& .${hke.label}`]:{[`&.${hke.disabled}`]:{color:(n.vars||n).palette.text.disabled}},variants:[{props:{labelPlacement:"start"},style:{flexDirection:"row-reverse",marginRight:-11}},{props:{labelPlacement:"top"},style:{flexDirection:"column-reverse"}},{props:{labelPlacement:"bottom"},style:{flexDirection:"column"}},{props:({labelPlacement:e})=>e==="start"||e==="top"||e==="bottom",style:{marginLeft:16}}]}))),Bbr=nn("span",{name:"MuiFormControlLabel",slot:"Asterisk"})(Gs(({theme:n})=>({[`&.${hke.error}`]:{color:(n.vars||n).palette.error.main}}))),FL=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiFormControlLabel"}),{checked:r,className:o,componentsProps:l={},control:c,disabled:d,disableTypography:h,inputRef:p,label:m,labelPlacement:b="end",name:w,onChange:_,required:x,slots:T={},slotProps:I={},value:D,...A}=i,M=DM(),O=d??c.props.disabled??M?.disabled,F=x??c.props.required,j={disabled:O,required:F};["checked","name","onChange","value","inputRef"].forEach(ie=>{typeof c.props[ie]>"u"&&typeof i[ie]<"u"&&(j[ie]=i[ie])});const W=HY({props:i,muiFormControl:M,states:["error"]}),U={...i,disabled:O,labelPlacement:b,required:F,error:W.error},Z=jbr(U),te={slots:T,slotProps:{...l,...I}},[G,ee]=_o("typography",{elementType:ei,externalForwardedProps:te,ownerState:U});let Q=m;return Q!=null&&Q.type!==ei&&!h&&(Q=k.jsx(G,{component:"span",...ee,className:_i(Z.label,ee?.className),children:Q})),k.jsxs(Hbr,{className:_i(Z.root,o),ownerState:U,ref:t,...A,children:[L.cloneElement(c,j),F?k.jsxs("div",{children:[Q,k.jsxs(Bbr,{ownerState:U,"aria-hidden":!0,className:Z.asterisk,children:[" ","*"]})]}):Q]})});function Wbr(n){return Po("MuiFormGroup",n)}Fo("MuiFormGroup",["root","row","error"]);const Vbr=n=>{const{classes:e,row:t,error:i}=n;return jo({root:["root",t&&"row",i&&"error"]},Wbr,e)},$br=nn("div",{name:"MuiFormGroup",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.row&&e.row]}})({display:"flex",flexDirection:"column",flexWrap:"wrap",variants:[{props:{row:!0},style:{flexDirection:"row"}}]}),zbr=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiFormGroup"}),{className:r,row:o=!1,...l}=i,c=DM(),d=HY({props:i,muiFormControl:c,states:["error"]}),h={...i,row:o,error:d.error},p=Vbr(h);return k.jsx($br,{className:_i(p.root,r),ownerState:h,ref:t,...l})});function Ubr(n){return Po("MuiFormHelperText",n)}const zPn=Fo("MuiFormHelperText",["root","error","disabled","sizeSmall","sizeMedium","contained","focused","filled","required"]);var UPn;const qbr=n=>{const{classes:e,contained:t,size:i,disabled:r,error:o,filled:l,focused:c,required:d}=n,h={root:["root",r&&"disabled",o&&"error",i&&`size${ri(i)}`,t&&"contained",c&&"focused",l&&"filled",d&&"required"]};return jo(h,Ubr,e)},Gbr=nn("p",{name:"MuiFormHelperText",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.size&&e[`size${ri(t.size)}`],t.contained&&e.contained,t.filled&&e.filled]}})(Gs(({theme:n})=>({color:(n.vars||n).palette.text.secondary,...n.typography.caption,textAlign:"left",marginTop:3,marginRight:0,marginBottom:0,marginLeft:0,[`&.${zPn.disabled}`]:{color:(n.vars||n).palette.text.disabled},[`&.${zPn.error}`]:{color:(n.vars||n).palette.error.main},variants:[{props:{size:"small"},style:{marginTop:4}},{props:({ownerState:e})=>e.contained,style:{marginLeft:14,marginRight:14}}]}))),Kbr=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiFormHelperText"}),{children:r,className:o,component:l="p",disabled:c,error:d,filled:h,focused:p,margin:m,required:b,variant:w,..._}=i,x=DM(),T=HY({props:i,muiFormControl:x,states:["variant","size","disabled","error","filled","focused","required"]}),I={...i,component:l,contained:T.variant==="filled"||T.variant==="outlined",variant:T.variant,size:T.size,disabled:T.disabled,error:T.error,filled:T.filled,focused:T.focused,required:T.required};delete I.ownerState;const D=qbr(I);return k.jsx(Gbr,{as:l,className:_i(D.root,o),ref:t,..._,ownerState:I,children:r===" "?UPn||(UPn=k.jsx("span",{className:"notranslate","aria-hidden":!0,children:"​"})):r})});function Ybr(n){return Po("MuiFormLabel",n)}const a4e=Fo("MuiFormLabel",["root","colorSecondary","focused","disabled","error","filled","required","asterisk"]),Zbr=n=>{const{classes:e,color:t,focused:i,disabled:r,error:o,filled:l,required:c}=n,d={root:["root",`color${ri(t)}`,r&&"disabled",o&&"error",l&&"filled",i&&"focused",c&&"required"],asterisk:["asterisk",o&&"error"]};return jo(d,Ybr,e)},Xbr=nn("label",{name:"MuiFormLabel",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.color==="secondary"&&e.colorSecondary,t.filled&&e.filled]}})(Gs(({theme:n})=>({color:(n.vars||n).palette.text.secondary,...n.typography.body1,lineHeight:"1.4375em",padding:0,position:"relative",variants:[...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{[`&.${a4e.focused}`]:{color:(n.vars||n).palette[e].main}}})),{props:{},style:{[`&.${a4e.disabled}`]:{color:(n.vars||n).palette.text.disabled},[`&.${a4e.error}`]:{color:(n.vars||n).palette.error.main}}}]}))),Qbr=nn("span",{name:"MuiFormLabel",slot:"Asterisk"})(Gs(({theme:n})=>({[`&.${a4e.error}`]:{color:(n.vars||n).palette.error.main}}))),Jbr=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiFormLabel"}),{children:r,className:o,color:l,component:c="label",disabled:d,error:h,filled:p,focused:m,required:b,...w}=i,_=DM(),x=HY({props:i,muiFormControl:_,states:["color","required","focused","disabled","error","filled"]}),T={...i,color:x.color||"primary",component:c,disabled:x.disabled,error:x.error,filled:x.filled,focused:x.focused,required:x.required},I=Zbr(T);return k.jsxs(Xbr,{as:c,ownerState:T,className:_i(I.root,o),ref:t,...w,children:[r,x.required&&k.jsxs(Qbr,{ownerState:T,"aria-hidden":!0,className:I.asterisk,children:[" ","*"]})]})}),yi=ohr({createStyledComponent:nn("div",{name:"MuiGrid",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.container&&e.container]}}),componentName:"MuiGrid",useThemeProps:n=>Vo({props:n,name:"MuiGrid"}),useTheme:Tf});function JOt(n){return`scale(${n}, ${n**2})`}const evr={entering:{opacity:1,transform:JOt(1)},entered:{opacity:1,transform:"none"}},BEt=typeof navigator<"u"&&/^((?!chrome|android).)*(safari|mobile)/i.test(navigator.userAgent)&&/(os |version\/)15(.|_)4/i.test(navigator.userAgent),wW=L.forwardRef(function(e,t){const{addEndListener:i,appear:r=!0,children:o,easing:l,in:c,onEnter:d,onEntered:h,onEntering:p,onExit:m,onExited:b,onExiting:w,style:_,timeout:x="auto",TransitionComponent:T=o8,...I}=e,D=PG(),A=L.useRef(),M=Tf(),O=L.useRef(null),F=xm(O,jY(o),t),j=ie=>se=>{if(ie){const ue=O.current;se===void 0?ie(ue):ie(ue,se)}},W=j(p),U=j((ie,se)=>{Gzt(ie);const{duration:ue,delay:ne,easing:we}=JK({style:_,timeout:x,easing:l},{mode:"enter"});let de;x==="auto"?(de=M.transitions.getAutoHeightDuration(ie.clientHeight),A.current=de):de=ue,ie.style.transition=[M.transitions.create("opacity",{duration:de,delay:ne}),M.transitions.create("transform",{duration:BEt?de:de*.666,delay:ne,easing:we})].join(","),d&&d(ie,se)}),Z=j(h),te=j(w),G=j(ie=>{const{duration:se,delay:ue,easing:ne}=JK({style:_,timeout:x,easing:l},{mode:"exit"});let we;x==="auto"?(we=M.transitions.getAutoHeightDuration(ie.clientHeight),A.current=we):we=se,ie.style.transition=[M.transitions.create("opacity",{duration:we,delay:ue}),M.transitions.create("transform",{duration:BEt?we:we*.666,delay:BEt?ue:ue||we*.333,easing:ne})].join(","),ie.style.opacity=0,ie.style.transform=JOt(.75),m&&m(ie)}),ee=j(b),Q=ie=>{x==="auto"&&D.start(A.current||0,ie),i&&i(O.current,ie)};return k.jsx(T,{appear:r,in:c,nodeRef:O,onEnter:U,onEntered:Z,onEntering:W,onExit:G,onExited:ee,onExiting:te,addEndListener:Q,timeout:x==="auto"?null:x,...I,children:(ie,{ownerState:se,...ue})=>L.cloneElement(o,{style:{opacity:0,transform:JOt(.75),visibility:ie==="exited"&&!c?"hidden":void 0,...evr[ie],..._,...o.props.style},ref:F,...ue})})});wW&&(wW.muiSupportAuto=!0);const tvr=n=>{const{classes:e,disableUnderline:t}=n,r=jo({root:["root",!t&&"underline"],input:["input"]},g1r,e);return{...e,...r}},nvr=nn(Get,{shouldForwardProp:n=>$_(n)||n==="classes",name:"MuiInput",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[...Uet(n,e),!t.disableUnderline&&e.underline]}})(Gs(({theme:n})=>{let t=n.palette.mode==="light"?"rgba(0, 0, 0, 0.42)":"rgba(255, 255, 255, 0.7)";return n.vars&&(t=n.alpha(n.vars.palette.common.onBackground,n.vars.opacity.inputUnderline)),{position:"relative",variants:[{props:({ownerState:i})=>i.formControl,style:{"label + &":{marginTop:16}}},{props:({ownerState:i})=>!i.disableUnderline,style:{"&::after":{left:0,bottom:0,content:'""',position:"absolute",right:0,transform:"scaleX(0)",transition:n.transitions.create("transform",{duration:n.transitions.duration.shorter,easing:n.transitions.easing.easeOut}),pointerEvents:"none"},[`&.${_G.focused}:after`]:{transform:"scaleX(1) translateX(0)"},[`&.${_G.error}`]:{"&::before, &::after":{borderBottomColor:(n.vars||n).palette.error.main}},"&::before":{borderBottom:`1px solid ${t}`,left:0,bottom:0,content:'"\\00a0"',position:"absolute",right:0,transition:n.transitions.create("border-bottom-color",{duration:n.transitions.duration.shorter}),pointerEvents:"none"},[`&:hover:not(.${_G.disabled}, .${_G.error}):before`]:{borderBottom:`2px solid ${(n.vars||n).palette.text.primary}`,"@media (hover: none)":{borderBottom:`1px solid ${t}`}},[`&.${_G.disabled}:before`]:{borderBottomStyle:"dotted"}}},...Object.entries(n.palette).filter(Vh()).map(([i])=>({props:{color:i,disableUnderline:!1},style:{"&::after":{borderBottom:`2px solid ${(n.vars||n).palette[i].main}`}}}))]}})),ivr=nn(Ket,{name:"MuiInput",slot:"Input",overridesResolver:qet})({}),cUt=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiInput"}),{disableUnderline:r=!1,components:o={},componentsProps:l,fullWidth:c=!1,inputComponent:d="input",multiline:h=!1,slotProps:p,slots:m={},type:b="text",...w}=i,_=tvr(i),T={root:{ownerState:{disableUnderline:r}}},I=p??l?F_(p??l,T):T,D=m.root??o.Root??nvr,A=m.input??o.Input??ivr;return k.jsx(U1e,{slots:{root:D,input:A},slotProps:I,fullWidth:c,inputComponent:d,multiline:h,ref:t,type:b,...w,classes:_})});cUt.muiName="Input";function rvr(n){return Po("MuiInputAdornment",n)}const qPn=Fo("MuiInputAdornment",["root","filled","standard","outlined","positionStart","positionEnd","disablePointerEvents","hiddenLabel","sizeSmall"]);var GPn;const svr=(n,e)=>{const{ownerState:t}=n;return[e.root,e[`position${ri(t.position)}`],t.disablePointerEvents===!0&&e.disablePointerEvents,e[t.variant]]},ovr=n=>{const{classes:e,disablePointerEvents:t,hiddenLabel:i,position:r,size:o,variant:l}=n,c={root:["root",t&&"disablePointerEvents",r&&`position${ri(r)}`,l,i&&"hiddenLabel",o&&`size${ri(o)}`]};return jo(c,rvr,e)},avr=nn("div",{name:"MuiInputAdornment",slot:"Root",overridesResolver:svr})(Gs(({theme:n})=>({display:"flex",maxHeight:"2em",alignItems:"center",whiteSpace:"nowrap",color:(n.vars||n).palette.action.active,variants:[{props:{variant:"filled"},style:{[`&.${qPn.positionStart}&:not(.${qPn.hiddenLabel})`]:{marginTop:16}}},{props:{position:"start"},style:{marginRight:8}},{props:{position:"end"},style:{marginLeft:8}},{props:{disablePointerEvents:!0},style:{pointerEvents:"none"}}]}))),v5e=L.forwardRef(function(e,t){const i=Vo({props:e,name:"MuiInputAdornment"}),{children:r,className:o,component:l="div",disablePointerEvents:c=!1,disableTypography:d=!1,position:h,variant:p,...m}=i,b=DM()||{};let w=p;p&&b.variant,b&&!w&&(w=b.variant);const _={...i,hiddenLabel:b.hiddenLabel,size:b.size,disablePointerEvents:c,position:h,variant:w},x=ovr(_);return k.jsx(zet.Provider,{value:null,children:k.jsx(avr,{as:l,ownerState:_,className:_i(x.root,o),ref:t,...m,children:typeof r=="string"&&!d?k.jsx(ei,{color:"textSecondary",children:r}):k.jsxs(L.Fragment,{children:[h==="start"?GPn||(GPn=k.jsx("span",{className:"notranslate","aria-hidden":!0,children:"​"})):null,r]})})})});function lvr(n){return Po("MuiInputLabel",n)}Fo("MuiInputLabel",["root","focused","disabled","error","required","asterisk","formControl","sizeSmall","shrink","animated","standard","filled","outlined"]);const cvr=n=>{const{classes:e,formControl:t,size:i,shrink:r,disableAnimation:o,variant:l,required:c}=n,d={root:["root",t&&"formControl",!o&&"animated",r&&"shrink",i&&i!=="medium"&&`size${ri(i)}`,l],asterisk:[c&&"asterisk"]},h=jo(d,lvr,e);return{...e,...h}},uvr=nn(Jbr,{shouldForwardProp:n=>$_(n)||n==="classes",name:"MuiInputLabel",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[{[`& .${a4e.asterisk}`]:e.asterisk},e.root,t.formControl&&e.formControl,t.size==="small"&&e.sizeSmall,t.shrink&&e.shrink,!t.disableAnimation&&e.animated,t.focused&&e.focused,e[t.variant]]}})(Gs(({theme:n})=>({display:"block",transformOrigin:"top left",whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:"100%",variants:[{props:({ownerState:e})=>e.formControl,style:{position:"absolute",left:0,top:0,transform:"translate(0, 20px) scale(1)"}},{props:{size:"small"},style:{transform:"translate(0, 17px) scale(1)"}},{props:({ownerState:e})=>e.shrink,style:{transform:"translate(0, -1.5px) scale(0.75)",transformOrigin:"top left",maxWidth:"133%"}},{props:({ownerState:e})=>!e.disableAnimation,style:{transition:n.transitions.create(["color","transform","max-width"],{duration:n.transitions.duration.shorter,easing:n.transitions.easing.easeOut})}},{props:{variant:"filled"},style:{zIndex:1,pointerEvents:"none",transform:"translate(12px, 16px) scale(1)",maxWidth:"calc(100% - 24px)"}},{props:{variant:"filled",size:"small"},style:{transform:"translate(12px, 13px) scale(1)"}},{props:({variant:e,ownerState:t})=>e==="filled"&&t.shrink,style:{userSelect:"none",pointerEvents:"auto",transform:"translate(12px, 7px) scale(0.75)",maxWidth:"calc(133% - 24px)"}},{props:({variant:e,ownerState:t,size:i})=>e==="filled"&&t.shrink&&i==="small",style:{transform:"translate(12px, 4px) scale(0.75)"}},{props:{variant:"outlined"},style:{zIndex:1,pointerEvents:"none",transform:"translate(14px, 16px) scale(1)",maxWidth:"calc(100% - 24px)"}},{props:{variant:"outlined",size:"small"},style:{transform:"translate(14px, 9px) scale(1)"}},{props:({variant:e,ownerState:t})=>e==="outlined"&&t.shrink,style:{userSelect:"none",pointerEvents:"auto",maxWidth:"calc(133% - 32px)",transform:"translate(14px, -9px) scale(0.75)"}}]}))),ese=L.forwardRef(function(e,t){const i=Vo({name:"MuiInputLabel",props:e}),{disableAnimation:r=!1,margin:o,shrink:l,variant:c,className:d,...h}=i,p=DM();let m=l;typeof m>"u"&&p&&(m=p.filled||p.focused||p.adornedStart);const b=HY({props:i,muiFormControl:p,states:["size","variant","required","focused"]}),w={...i,disableAnimation:r,formControl:p,shrink:m,size:b.size,variant:b.variant,required:b.required,focused:b.focused},_=cvr(w);return k.jsx(uvr,{"data-shrink":m,ref:t,className:_i(_.root,d),...h,ownerState:w,classes:_})});function dvr(n){return Po("MuiLinearProgress",n)}Fo("MuiLinearProgress",["root","colorPrimary","colorSecondary","determinate","indeterminate","buffer","query","dashed","dashedColorPrimary","dashedColorSecondary","bar","bar1","bar2","barColorPrimary","barColorSecondary","bar1Indeterminate","bar1Determinate","bar1Buffer","bar2Indeterminate","bar2Buffer"]);const eNt=4,tNt=W3` 0% { left: -35%; right: 100%; @@ -161,9 +161,9 @@ Error generating stack: `+je.message+` left: 100%; right: -90%; } -`,avr=typeof nNt!="string"?U1e` - animation: ${nNt} 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; - `:null,iNt=W3` +`,hvr=typeof tNt!="string"?W1e` + animation: ${tNt} 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; + `:null,nNt=W3` 0% { left: -200%; right: 100%; @@ -178,9 +178,9 @@ Error generating stack: `+je.message+` left: 107%; right: -8%; } -`,lvr=typeof iNt!="string"?U1e` - animation: ${iNt} 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) 1.15s infinite; - `:null,rNt=W3` +`,fvr=typeof nNt!="string"?W1e` + animation: ${nNt} 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) 1.15s infinite; + `:null,iNt=W3` 0% { opacity: 1; background-position: 0 -23px; @@ -195,52 +195,52 @@ Error generating stack: `+je.message+` opacity: 1; background-position: -200px -23px; } -`,cvr=typeof rNt!="string"?U1e` - animation: ${rNt} 3s infinite linear; - `:null,uvr=n=>{const{classes:e,variant:t,color:i}=n,r={root:["root",`color${ii(i)}`,t],dashed:["dashed",`dashedColor${ii(i)}`],bar1:["bar","bar1",`barColor${ii(i)}`,(t==="indeterminate"||t==="query")&&"bar1Indeterminate",t==="determinate"&&"bar1Determinate",t==="buffer"&&"bar1Buffer"],bar2:["bar","bar2",t!=="buffer"&&`barColor${ii(i)}`,t==="buffer"&&`color${ii(i)}`,(t==="indeterminate"||t==="query")&&"bar2Indeterminate",t==="buffer"&&"bar2Buffer"]};return Fo(r,ovr,e)},hUt=(n,e)=>n.vars?n.vars.palette.LinearProgress[`${e}Bg`]:n.palette.mode==="light"?n.lighten(n.palette[e].main,.62):n.darken(n.palette[e].main,.5),dvr=tn("span",{name:"MuiLinearProgress",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[`color${ii(t.color)}`],e[t.variant]]}})(Gs(({theme:n})=>({position:"relative",overflow:"hidden",display:"block",height:4,zIndex:0,"@media print":{colorAdjust:"exact"},variants:[...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{backgroundColor:hUt(n,e)}})),{props:({ownerState:e})=>e.color==="inherit"&&e.variant!=="buffer",style:{"&::before":{content:'""',position:"absolute",left:0,top:0,right:0,bottom:0,backgroundColor:"currentColor",opacity:.3}}},{props:{variant:"buffer"},style:{backgroundColor:"transparent"}},{props:{variant:"query"},style:{transform:"rotate(180deg)"}}]}))),hvr=tn("span",{name:"MuiLinearProgress",slot:"Dashed",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.dashed,e[`dashedColor${ii(t.color)}`]]}})(Gs(({theme:n})=>({position:"absolute",marginTop:0,height:"100%",width:"100%",backgroundSize:"10px 10px",backgroundPosition:"0 -23px",variants:[{props:{color:"inherit"},style:{opacity:.3,backgroundImage:"radial-gradient(currentColor 0%, currentColor 16%, transparent 42%)"}},...Object.entries(n.palette).filter(Vh()).map(([e])=>{const t=hUt(n,e);return{props:{color:e},style:{backgroundImage:`radial-gradient(${t} 0%, ${t} 16%, transparent 42%)`}}})]})),cvr||{animation:`${rNt} 3s infinite linear`}),fvr=tn("span",{name:"MuiLinearProgress",slot:"Bar1",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.bar,e.bar1,e[`barColor${ii(t.color)}`],(t.variant==="indeterminate"||t.variant==="query")&&e.bar1Indeterminate,t.variant==="determinate"&&e.bar1Determinate,t.variant==="buffer"&&e.bar1Buffer]}})(Gs(({theme:n})=>({width:"100%",position:"absolute",left:0,bottom:0,top:0,transition:"transform 0.2s linear",transformOrigin:"left",variants:[{props:{color:"inherit"},style:{backgroundColor:"currentColor"}},...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{backgroundColor:(n.vars||n).palette[e].main}})),{props:{variant:"determinate"},style:{transition:`transform .${tNt}s linear`}},{props:{variant:"buffer"},style:{zIndex:1,transition:`transform .${tNt}s linear`}},{props:({ownerState:e})=>e.variant==="indeterminate"||e.variant==="query",style:{width:"auto"}},{props:({ownerState:e})=>e.variant==="indeterminate"||e.variant==="query",style:avr||{animation:`${nNt} 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite`}}]}))),pvr=tn("span",{name:"MuiLinearProgress",slot:"Bar2",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.bar,e.bar2,e[`barColor${ii(t.color)}`],(t.variant==="indeterminate"||t.variant==="query")&&e.bar2Indeterminate,t.variant==="buffer"&&e.bar2Buffer]}})(Gs(({theme:n})=>({width:"100%",position:"absolute",left:0,bottom:0,top:0,transition:"transform 0.2s linear",transformOrigin:"left",variants:[...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{"--LinearProgressBar2-barColor":(n.vars||n).palette[e].main}})),{props:({ownerState:e})=>e.variant!=="buffer"&&e.color!=="inherit",style:{backgroundColor:"var(--LinearProgressBar2-barColor, currentColor)"}},{props:({ownerState:e})=>e.variant!=="buffer"&&e.color==="inherit",style:{backgroundColor:"currentColor"}},{props:{color:"inherit"},style:{opacity:.3}},...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e,variant:"buffer"},style:{backgroundColor:hUt(n,e),transition:`transform .${tNt}s linear`}})),{props:({ownerState:e})=>e.variant==="indeterminate"||e.variant==="query",style:{width:"auto"}},{props:({ownerState:e})=>e.variant==="indeterminate"||e.variant==="query",style:lvr||{animation:`${iNt} 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) 1.15s infinite`}}]}))),_ui=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiLinearProgress"}),{className:r,color:o="primary",value:l,valueBuffer:c,variant:d="indeterminate",...h}=i,p={...i,color:o,variant:d},m=uvr(p),b=MY(),w={},_={bar1:{},bar2:{}};if((d==="determinate"||d==="buffer")&&l!==void 0){w["aria-valuenow"]=Math.round(l),w["aria-valuemin"]=0,w["aria-valuemax"]=100;let x=l-100;b&&(x=-x),_.bar1.transform=`translateX(${x}%)`}if(d==="buffer"&&c!==void 0){let x=(c||0)-100;b&&(x=-x),_.bar2.transform=`translateX(${x}%)`}return k.jsxs(dvr,{className:_i(m.root,r),ownerState:p,role:"progressbar",...w,ref:t,...h,children:[d==="buffer"?k.jsx(hvr,{className:m.dashed,ownerState:p}):null,k.jsx(fvr,{className:m.bar1,ownerState:p,style:_.bar1}),d==="determinate"?null:k.jsx(pvr,{className:m.bar2,ownerState:p,style:_.bar2})]})});function gvr(n){return No("MuiLink",n)}const mvr=Po("MuiLink",["root","underlineNone","underlineHover","underlineAlways","button","focusVisible"]),bvr=({theme:n,ownerState:e})=>{const t=e.color;if("colorSpace"in n&&n.colorSpace){const o=jP(n,`palette.${t}.main`)||jP(n,`palette.${t}`)||e.color;return n.alpha(o,.4)}const i=jP(n,`palette.${t}.main`,!1)||jP(n,`palette.${t}`,!1)||e.color,r=jP(n,`palette.${t}.mainChannel`)||jP(n,`palette.${t}Channel`);return"vars"in n&&r?`rgba(${r} / 0.4)`:Wa(i,.4)},GPn={primary:!0,secondary:!0,error:!0,info:!0,success:!0,warning:!0,textPrimary:!0,textSecondary:!0,textDisabled:!0},vvr=n=>{const{classes:e,component:t,focusVisible:i,underline:r}=n,o={root:["root",`underline${ii(r)}`,t==="button"&&"button",i&&"focusVisible"]};return Fo(o,gvr,e)},wvr=tn(di,{name:"MuiLink",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[`underline${ii(t.underline)}`],t.component==="button"&&e.button]}})(Gs(({theme:n})=>({variants:[{props:{underline:"none"},style:{textDecoration:"none"}},{props:{underline:"hover"},style:{textDecoration:"none","&:hover":{textDecoration:"underline"}}},{props:{underline:"always"},style:{textDecoration:"underline","&:hover":{textDecorationColor:"inherit"}}},{props:({underline:e,ownerState:t})=>e==="always"&&t.color!=="inherit",style:{textDecorationColor:"var(--Link-underlineColor)"}},{props:({underline:e,ownerState:t})=>e==="always"&&t.color==="inherit",style:n.colorSpace?{textDecorationColor:n.alpha("currentColor",.4)}:null},...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{underline:"always",color:e},style:{"--Link-underlineColor":n.alpha((n.vars||n).palette[e].main,.4)}})),{props:{underline:"always",color:"textPrimary"},style:{"--Link-underlineColor":n.alpha((n.vars||n).palette.text.primary,.4)}},{props:{underline:"always",color:"textSecondary"},style:{"--Link-underlineColor":n.alpha((n.vars||n).palette.text.secondary,.4)}},{props:{underline:"always",color:"textDisabled"},style:{"--Link-underlineColor":(n.vars||n).palette.text.disabled}},{props:{component:"button"},style:{position:"relative",WebkitTapHighlightColor:"transparent",backgroundColor:"transparent",outline:0,border:0,margin:0,borderRadius:0,padding:0,cursor:"pointer",userSelect:"none",verticalAlign:"middle",MozAppearance:"none",WebkitAppearance:"none","&::-moz-focus-inner":{borderStyle:"none"},[`&.${mvr.focusVisible}`]:{outline:"auto"}}}]}))),W6=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiLink"}),r=Lf(),{className:o,color:l="primary",component:c="a",onBlur:d,onFocus:h,TypographyClasses:p,underline:m="always",variant:b="inherit",sx:w,..._}=i,[x,T]=D.useState(!1),I=O=>{JK(O.target)||T(!1),d&&d(O)},L=O=>{JK(O.target)&&T(!0),h&&h(O)},A={...i,color:l,component:c,focusVisible:x,underline:m,variant:b},M=vvr(A);return k.jsx(wvr,{color:l,className:_i(M.root,o),classes:p,component:c,onBlur:I,onFocus:L,ref:t,ownerState:A,variant:b,..._,sx:[...GPn[l]===void 0?[{color:l}]:[],...Array.isArray(w)?w:[w]],style:{..._.style,...m==="always"&&l!=="inherit"&&!GPn[l]&&{"--Link-underlineColor":bvr({theme:r,ownerState:A})}}})}),nM=D.createContext({});function yvr(n){return No("MuiList",n)}Po("MuiList",["root","padding","dense","subheader"]);const _vr=n=>{const{classes:e,disablePadding:t,dense:i,subheader:r}=n;return Fo({root:["root",!t&&"padding",i&&"dense",r&&"subheader"]},yvr,e)},Cvr=tn("ul",{name:"MuiList",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,!t.disablePadding&&e.padding,t.dense&&e.dense,t.subheader&&e.subheader]}})({listStyle:"none",margin:0,padding:0,position:"relative",variants:[{props:({ownerState:n})=>!n.disablePadding,style:{paddingTop:8,paddingBottom:8}},{props:({ownerState:n})=>n.subheader,style:{paddingTop:0}}]}),nse=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiList"}),{children:r,className:o,component:l="ul",dense:c=!1,disablePadding:d=!1,subheader:h,...p}=i,m=D.useMemo(()=>({dense:c}),[c]),b={...i,component:l,dense:c,disablePadding:d},w=_vr(b);return k.jsx(nM.Provider,{value:m,children:k.jsxs(Cvr,{as:l,className:_i(w.root,o),ref:t,ownerState:b,...p,children:[h,r]})})});function Svr(n){return No("MuiListItem",n)}Po("MuiListItem",["root","container","dense","alignItemsFlexStart","divider","gutters","padding","secondaryAction"]);function xvr(n){return No("MuiListItemButton",n)}const mfe=Po("MuiListItemButton",["root","focusVisible","dense","alignItemsFlexStart","disabled","divider","gutters","selected"]),Evr=(n,e)=>{const{ownerState:t}=n;return[e.root,t.dense&&e.dense,t.alignItems==="flex-start"&&e.alignItemsFlexStart,t.divider&&e.divider,!t.disableGutters&&e.gutters]},kvr=n=>{const{alignItems:e,classes:t,dense:i,disabled:r,disableGutters:o,divider:l,selected:c}=n,h=Fo({root:["root",i&&"dense",!o&&"gutters",l&&"divider",r&&"disabled",e==="flex-start"&&"alignItemsFlexStart",c&&"selected"]},xvr,t);return{...t,...h}},Tvr=tn(D3,{shouldForwardProp:n=>B_(n)||n==="classes",name:"MuiListItemButton",slot:"Root",overridesResolver:Evr})(Gs(({theme:n})=>({display:"flex",flexGrow:1,justifyContent:"flex-start",alignItems:"center",position:"relative",textDecoration:"none",minWidth:0,boxSizing:"border-box",textAlign:"left",paddingTop:8,paddingBottom:8,transition:n.transitions.create("background-color",{duration:n.transitions.duration.shortest}),"&:hover":{textDecoration:"none",backgroundColor:(n.vars||n).palette.action.hover,"@media (hover: none)":{backgroundColor:"transparent"}},[`&.${mfe.selected}`]:{backgroundColor:n.alpha((n.vars||n).palette.primary.main,(n.vars||n).palette.action.selectedOpacity),[`&.${mfe.focusVisible}`]:{backgroundColor:n.alpha((n.vars||n).palette.primary.main,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.focusOpacity}`)}},[`&.${mfe.selected}:hover`]:{backgroundColor:n.alpha((n.vars||n).palette.primary.main,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.hoverOpacity}`),"@media (hover: none)":{backgroundColor:n.alpha((n.vars||n).palette.primary.main,(n.vars||n).palette.action.selectedOpacity)}},[`&.${mfe.focusVisible}`]:{backgroundColor:(n.vars||n).palette.action.focus},[`&.${mfe.disabled}`]:{opacity:(n.vars||n).palette.action.disabledOpacity},variants:[{props:({ownerState:e})=>e.divider,style:{borderBottom:`1px solid ${(n.vars||n).palette.divider}`,backgroundClip:"padding-box"}},{props:{alignItems:"flex-start"},style:{alignItems:"flex-start"}},{props:({ownerState:e})=>!e.disableGutters,style:{paddingLeft:16,paddingRight:16}},{props:({ownerState:e})=>e.dense,style:{paddingTop:4,paddingBottom:4}}]}))),Lvr=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiListItemButton"}),{alignItems:r="center",autoFocus:o=!1,component:l="div",children:c,dense:d=!1,disableGutters:h=!1,divider:p=!1,focusVisibleClassName:m,selected:b=!1,className:w,..._}=i,x=D.useContext(nM),T=D.useMemo(()=>({dense:d||x.dense||!1,alignItems:r,disableGutters:h}),[r,x.dense,d,h]),I=D.useRef(null);IS(()=>{o&&I.current&&I.current.focus()},[o]);const L={...i,alignItems:r,dense:T.dense,disableGutters:h,divider:p,selected:b},A=kvr(L),M=xm(I,t);return k.jsx(nM.Provider,{value:T,children:k.jsx(Tvr,{ref:M,href:_.href||_.to,component:(_.href||_.to)&&l==="div"?"button":l,focusVisibleClassName:_i(A.focusVisible,m),ownerState:L,className:_i(A.root,w),..._,classes:A,children:c})})});function Dvr(n){return No("MuiListItemSecondaryAction",n)}Po("MuiListItemSecondaryAction",["root","disableGutters"]);const Ivr=n=>{const{disableGutters:e,classes:t}=n;return Fo({root:["root",e&&"disableGutters"]},Dvr,t)},Avr=tn("div",{name:"MuiListItemSecondaryAction",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.disableGutters&&e.disableGutters]}})({position:"absolute",right:16,top:"50%",transform:"translateY(-50%)",variants:[{props:({ownerState:n})=>n.disableGutters,style:{right:0}}]}),Cui=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiListItemSecondaryAction"}),{className:r,...o}=i,l=D.useContext(nM),c={...i,disableGutters:l.disableGutters},d=Ivr(c);return k.jsx(Avr,{className:_i(d.root,r),ownerState:c,ref:t,...o})});Cui.muiName="ListItemSecondaryAction";const Rvr=(n,e)=>{const{ownerState:t}=n;return[e.root,t.dense&&e.dense,t.alignItems==="flex-start"&&e.alignItemsFlexStart,t.divider&&e.divider,!t.disableGutters&&e.gutters,!t.disablePadding&&e.padding,t.hasSecondaryAction&&e.secondaryAction]},Mvr=n=>{const{alignItems:e,classes:t,dense:i,disableGutters:r,disablePadding:o,divider:l,hasSecondaryAction:c}=n;return Fo({root:["root",i&&"dense",!r&&"gutters",!o&&"padding",l&&"divider",e==="flex-start"&&"alignItemsFlexStart",c&&"secondaryAction"],container:["container"],secondaryAction:["secondaryAction"]},Svr,t)},Ovr=tn("div",{name:"MuiListItem",slot:"Root",overridesResolver:Rvr})(Gs(({theme:n})=>({display:"flex",justifyContent:"flex-start",alignItems:"center",position:"relative",textDecoration:"none",width:"100%",boxSizing:"border-box",textAlign:"left",variants:[{props:({ownerState:e})=>!e.disablePadding,style:{paddingTop:8,paddingBottom:8}},{props:({ownerState:e})=>!e.disablePadding&&e.dense,style:{paddingTop:4,paddingBottom:4}},{props:({ownerState:e})=>!e.disablePadding&&!e.disableGutters,style:{paddingLeft:16,paddingRight:16}},{props:({ownerState:e})=>!e.disablePadding&&!!e.secondaryAction,style:{paddingRight:48}},{props:({ownerState:e})=>!!e.secondaryAction,style:{[`& > .${mfe.root}`]:{paddingRight:48}}},{props:{alignItems:"flex-start"},style:{alignItems:"flex-start"}},{props:({ownerState:e})=>e.divider,style:{borderBottom:`1px solid ${(n.vars||n).palette.divider}`,backgroundClip:"padding-box"}},{props:({ownerState:e})=>e.button,style:{transition:n.transitions.create("background-color",{duration:n.transitions.duration.shortest}),"&:hover":{textDecoration:"none",backgroundColor:(n.vars||n).palette.action.hover,"@media (hover: none)":{backgroundColor:"transparent"}}}},{props:({ownerState:e})=>e.hasSecondaryAction,style:{paddingRight:48}}]}))),Nvr=tn("li",{name:"MuiListItem",slot:"Container"})({position:"relative"}),u4e=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiListItem"}),{alignItems:r="center",children:o,className:l,component:c,components:d={},componentsProps:h={},ContainerComponent:p="li",ContainerProps:{className:m,...b}={},dense:w=!1,disableGutters:_=!1,disablePadding:x=!1,divider:T=!1,secondaryAction:I,slotProps:L={},slots:A={},...M}=i,O=D.useContext(nM),F=D.useMemo(()=>({dense:w||O.dense||!1,alignItems:r,disableGutters:_}),[r,O.dense,w,_]),j=D.useRef(null),W=D.Children.toArray(o),q=W.length&&n4e(W[W.length-1],["ListItemSecondaryAction"]),Z={...i,alignItems:r,dense:F.dense,disableGutters:_,disablePadding:x,divider:T,hasSecondaryAction:q},ee=Mvr(Z),G=xm(j,t),te={slots:A,slotProps:L},[Q,ie]=_o("secondaryAction",{elementType:Cui,externalForwardedProps:te,ownerState:Z,className:ee.secondaryAction}),se=A.root||d.Root||Ovr,de=L.root||h.root||{},ne={className:_i(ee.root,de.className,l),...M};let we=c||"li";return q?(we=!ne.component&&!c?"div":we,p==="li"&&(we==="li"?we="div":ne.component==="li"&&(ne.component="div")),k.jsx(nM.Provider,{value:F,children:k.jsxs(Nvr,{as:p,className:_i(ee.container,m),ref:G,ownerState:Z,...b,children:[k.jsx(se,{...de,...!x9(se)&&{as:we,ownerState:{...Z,...de.ownerState}},...ne,children:W}),W.pop()]})})):k.jsx(nM.Provider,{value:F,children:k.jsxs(se,{...de,as:we,ref:G,...!x9(se)&&{ownerState:{...Z,...de.ownerState}},...ne,children:[W,I&&k.jsx(Q,{...ie,children:I})]})})});function Pvr(n){return No("MuiListItemIcon",n)}const KPn=Po("MuiListItemIcon",["root","alignItemsFlexStart"]),Fvr=n=>{const{alignItems:e,classes:t}=n;return Fo({root:["root",e==="flex-start"&&"alignItemsFlexStart"]},Pvr,t)},jvr=tn("div",{name:"MuiListItemIcon",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.alignItems==="flex-start"&&e.alignItemsFlexStart]}})(Gs(({theme:n})=>({minWidth:56,color:(n.vars||n).palette.action.active,flexShrink:0,display:"inline-flex",variants:[{props:{alignItems:"flex-start"},style:{marginTop:8}}]}))),Sui=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiListItemIcon"}),{className:r,...o}=i,l=D.useContext(nM),c={...i,alignItems:l.alignItems},d=Fvr(c);return k.jsx(jvr,{className:_i(d.root,r),ownerState:c,ref:t,...o})});function Hvr(n){return No("MuiListItemText",n)}const Wfe=Po("MuiListItemText",["root","multiline","dense","inset","primary","secondary"]),Bvr=n=>{const{classes:e,inset:t,primary:i,secondary:r,dense:o}=n;return Fo({root:["root",t&&"inset",o&&"dense",i&&r&&"multiline"],primary:["primary"],secondary:["secondary"]},Hvr,e)},Wvr=tn("div",{name:"MuiListItemText",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[{[`& .${Wfe.primary}`]:e.primary},{[`& .${Wfe.secondary}`]:e.secondary},e.root,t.inset&&e.inset,t.primary&&t.secondary&&e.multiline,t.dense&&e.dense]}})({flex:"1 1 auto",minWidth:0,marginTop:4,marginBottom:4,[`.${fPn.root}:where(& .${Wfe.primary}), .${fPn.root}:where(& .${Wfe.secondary})`]:{display:"block"},variants:[{props:({ownerState:n})=>n.primary&&n.secondary,style:{marginTop:6,marginBottom:6}},{props:({ownerState:n})=>n.inset,style:{paddingLeft:56}}]}),aGe=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiListItemText"}),{children:r,className:o,disableTypography:l=!1,inset:c=!1,primary:d,primaryTypographyProps:h,secondary:p,secondaryTypographyProps:m,slots:b={},slotProps:w={},..._}=i,{dense:x}=D.useContext(nM);let T=d??r,I=p;const L={...i,disableTypography:l,inset:c,primary:!!T,secondary:!!I,dense:x},A=Bvr(L),M={slots:b,slotProps:{primary:h,secondary:m,...w}},[O,F]=_o("root",{className:_i(A.root,o),elementType:Wvr,externalForwardedProps:{...M,..._},ownerState:L,ref:t}),[j,W]=_o("primary",{className:A.primary,elementType:di,externalForwardedProps:M,ownerState:L}),[q,Z]=_o("secondary",{className:A.secondary,elementType:di,externalForwardedProps:M,ownerState:L});return T!=null&&T.type!==di&&!l&&(T=k.jsx(j,{variant:x?"body2":"body1",component:W?.variant?void 0:"span",...W,children:T})),I!=null&&I.type!==di&&!l&&(I=k.jsx(q,{variant:"body2",color:"textSecondary",...Z,children:I})),k.jsxs(O,{...F,children:[T,I]})});function WEt(n,e,t){return n===e?n.firstChild:e&&e.nextElementSibling?e.nextElementSibling:t?null:n.firstChild}function YPn(n,e,t){return n===e?t?n.firstChild:n.lastChild:e&&e.previousElementSibling?e.previousElementSibling:t?null:n.lastChild}function xui(n,e){if(e===void 0)return!0;let t=n.innerText;return t===void 0&&(t=n.textContent),t=t.trim().toLowerCase(),t.length===0?!1:e.repeating?t[0]===e.keys[0]:t.startsWith(e.keys.join(""))}function jxe(n,e,t,i,r,o){let l=!1,c=r(n,e,e?t:!1);for(;c;){if(c===n.firstChild){if(l)return!1;l=!0}const d=i?!1:c.disabled||c.getAttribute("aria-disabled")==="true";if(!c.hasAttribute("tabindex")||!xui(c,o)||d)c=r(n,c,t);else return c.focus(),!0}return!1}const Z1e=D.forwardRef(function(e,t){const{actions:i,autoFocus:r=!1,autoFocusItem:o=!1,children:l,className:c,disabledItemsFocusable:d=!1,disableListWrap:h=!1,onKeyDown:p,variant:m="selectedMenu",...b}=e,w=D.useRef(null),_=D.useRef({keys:[],repeating:!0,previousKeyMatched:!0,lastTime:null});IS(()=>{r&&w.current.focus()},[r]),D.useImperativeHandle(i,()=>({adjustStyleForScrollbar:(A,{direction:M})=>{const O=!w.current.style.width;if(A.clientHeight{const M=w.current,O=A.key;if(A.ctrlKey||A.metaKey||A.altKey){p&&p(A);return}const j=Bfe(hv(M));if(O==="ArrowDown")A.preventDefault(),jxe(M,j,h,d,WEt);else if(O==="ArrowUp")A.preventDefault(),jxe(M,j,h,d,YPn);else if(O==="Home")A.preventDefault(),jxe(M,null,h,d,WEt);else if(O==="End")A.preventDefault(),jxe(M,null,h,d,YPn);else if(O.length===1){const W=_.current,q=O.toLowerCase(),Z=performance.now();W.keys.length>0&&(Z-W.lastTime>500?(W.keys=[],W.repeating=!0,W.previousKeyMatched=!0):W.repeating&&q!==W.keys[0]&&(W.repeating=!1)),W.lastTime=Z,W.keys.push(q);const ee=j&&!W.repeating&&xui(j,W);W.previousKeyMatched&&(ee||jxe(M,j,!1,d,WEt,W))?A.preventDefault():W.previousKeyMatched=!1}p&&p(A)},T=xm(w,t);let I=-1;D.Children.forEach(l,(A,M)=>{if(!D.isValidElement(A)){I===M&&(I+=1,I>=l.length&&(I=-1));return}A.props.disabled||(m==="selectedMenu"&&A.props.selected||I===-1)&&(I=M),I===M&&(A.props.disabled||A.props.muiSkipListHighlight||A.type.muiSkipListHighlight)&&(I+=1,I>=l.length&&(I=-1))});const L=D.Children.map(l,(A,M)=>{if(M===I){const O={};return o&&(O.autoFocus=!0),A.props.tabIndex===void 0&&m==="selectedMenu"&&(O.tabIndex=0),D.cloneElement(A,O)}return A});return k.jsx(nse,{role:"menu",ref:T,className:c,onKeyDown:x,tabIndex:r?0:-1,...b,children:L})});function Vvr(n){return No("MuiPopover",n)}Po("MuiPopover",["root","paper"]);function ZPn(n,e){let t=0;return typeof e=="number"?t=e:e==="center"?t=n.height/2:e==="bottom"&&(t=n.height),t}function XPn(n,e){let t=0;return typeof e=="number"?t=e:e==="center"?t=n.width/2:e==="right"&&(t=n.width),t}function QPn(n){return[n.horizontal,n.vertical].map(e=>typeof e=="number"?`${e}px`:e).join(" ")}function FBe(n){return typeof n=="function"?n():n}const $vr=n=>{const{classes:e}=n;return Fo({root:["root"],paper:["paper"]},Vvr,e)},zvr=tn(Yet,{name:"MuiPopover",slot:"Root"})({}),Eui=tn(Jf,{name:"MuiPopover",slot:"Paper"})({position:"absolute",overflowY:"auto",overflowX:"hidden",minWidth:16,minHeight:16,maxWidth:"calc(100% - 32px)",maxHeight:"calc(100% - 32px)",outline:0}),fUt=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiPopover"}),{action:r,anchorEl:o,anchorOrigin:l={vertical:"top",horizontal:"left"},anchorPosition:c,anchorReference:d="anchorEl",children:h,className:p,container:m,elevation:b=8,marginThreshold:w=16,open:_,PaperProps:x={},slots:T={},slotProps:I={},transformOrigin:L={vertical:"top",horizontal:"left"},TransitionComponent:A,transitionDuration:M="auto",TransitionProps:O={},disableScrollLock:F=!1,...j}=i,W=D.useRef(),q={...i,anchorOrigin:l,anchorReference:d,elevation:b,marginThreshold:w,transformOrigin:L,TransitionComponent:A,transitionDuration:M,TransitionProps:O},Z=$vr(q),ee=D.useCallback(()=>{if(d==="anchorPosition")return c;const tt=FBe(o),Me=(tt&&tt.nodeType===1?tt:hv(W.current).body).getBoundingClientRect();return{top:Me.top+ZPn(Me,l.vertical),left:Me.left+XPn(Me,l.horizontal)}},[o,l.horizontal,l.vertical,c,d]),G=D.useCallback(tt=>({vertical:ZPn(tt,L.vertical),horizontal:XPn(tt,L.horizontal)}),[L.horizontal,L.vertical]),te=D.useCallback(tt=>{const Ue={width:tt.offsetWidth,height:tt.offsetHeight},Me=G(Ue);if(d==="none")return{top:null,left:null,transformOrigin:QPn(Me)};const He=ee();let at=He.top-Me.vertical,rt=He.left-Me.horizontal;const Be=at+Ue.height,lt=rt+Ue.width,ct=Y6(FBe(o)),ze=ct.innerHeight-w,Ke=ct.innerWidth-w;if(w!==null&&atze){const $e=Be-ze;at-=$e,Me.vertical+=$e}if(w!==null&&rtKe){const $e=lt-Ke;rt-=$e,Me.horizontal+=$e}return{top:`${Math.round(at)}px`,left:`${Math.round(rt)}px`,transformOrigin:QPn(Me)}},[o,d,ee,G,w]),[Q,ie]=D.useState(_),se=D.useCallback(()=>{const tt=W.current;if(!tt)return;const Ue=te(tt);Ue.top!==null&&tt.style.setProperty("top",Ue.top),Ue.left!==null&&(tt.style.left=Ue.left),tt.style.transformOrigin=Ue.transformOrigin,ie(!0)},[te]);D.useEffect(()=>(F&&window.addEventListener("scroll",se),()=>window.removeEventListener("scroll",se)),[o,F,se]);const de=()=>{se()},ne=()=>{ie(!1)};D.useEffect(()=>{_&&se()}),D.useImperativeHandle(r,()=>_?{updatePosition:()=>{se()}}:null,[_,se]),D.useEffect(()=>{if(!_)return;const tt=g5e(()=>{se()}),Ue=Y6(FBe(o));return Ue.addEventListener("resize",tt),()=>{tt.clear(),Ue.removeEventListener("resize",tt)}},[o,_,se]);let we=M;const ue={slots:{transition:A,...T},slotProps:{transition:O,paper:x,...I}},[ce,ye]=_o("transition",{elementType:yW,externalForwardedProps:ue,ownerState:q,getSlotProps:tt=>({...tt,onEntering:(Ue,Me)=>{tt.onEntering?.(Ue,Me),de()},onExited:Ue=>{tt.onExited?.(Ue),ne()}}),additionalProps:{appear:!0,in:_}});M==="auto"&&!ce.muiSupportAuto&&(we=void 0);const he=m||(o?hv(FBe(o)).body:void 0),[pe,{slots:me,slotProps:be,...xe}]=_o("root",{ref:t,elementType:zvr,externalForwardedProps:{...ue,...j},shouldForwardComponentProp:!0,additionalProps:{slots:{backdrop:T.backdrop},slotProps:{backdrop:Uzt(typeof I.backdrop=="function"?I.backdrop(q):I.backdrop,{invisible:!0})},container:he,open:_},ownerState:q,className:_i(Z.root,p)}),[Te,Ge]=_o("paper",{ref:W,className:Z.paper,elementType:Eui,externalForwardedProps:ue,shouldForwardComponentProp:!0,additionalProps:{elevation:b,style:Q?void 0:{opacity:0}},ownerState:q});return k.jsx(pe,{...xe,...!x9(pe)&&{slots:me,slotProps:be,disableScrollLock:F},children:k.jsx(ce,{...ye,timeout:we,children:k.jsx(Te,{...Ge,children:h})})})});function Uvr(n){return No("MuiMenu",n)}Po("MuiMenu",["root","paper","list"]);const qvr={vertical:"top",horizontal:"right"},Gvr={vertical:"top",horizontal:"left"},Kvr=n=>{const{classes:e}=n;return Fo({root:["root"],paper:["paper"],list:["list"]},Uvr,e)},Yvr=tn(fUt,{shouldForwardProp:n=>B_(n)||n==="classes",name:"MuiMenu",slot:"Root"})({}),Zvr=tn(Eui,{name:"MuiMenu",slot:"Paper"})({maxHeight:"calc(100% - 96px)",WebkitOverflowScrolling:"touch"}),Xvr=tn(Z1e,{name:"MuiMenu",slot:"List"})({outline:0}),pUt=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiMenu"}),{autoFocus:r=!0,children:o,className:l,disableAutoFocusItem:c=!1,MenuListProps:d={},onClose:h,open:p,PaperProps:m={},PopoverClasses:b,transitionDuration:w="auto",TransitionProps:{onEntering:_,...x}={},variant:T="selectedMenu",slots:I={},slotProps:L={},...A}=i,M=MY(),O={...i,autoFocus:r,disableAutoFocusItem:c,MenuListProps:d,onEntering:_,PaperProps:m,transitionDuration:w,TransitionProps:x,variant:T},F=Kvr(O),j=r&&!c&&p,W=D.useRef(null),q=(we,ue)=>{W.current&&W.current.adjustStyleForScrollbar(we,{direction:M?"rtl":"ltr"}),_&&_(we,ue)},Z=we=>{we.key==="Tab"&&(we.preventDefault(),h&&h(we,"tabKeyDown"))};let ee=-1;D.Children.map(o,(we,ue)=>{D.isValidElement(we)&&(we.props.disabled||(T==="selectedMenu"&&we.props.selected||ee===-1)&&(ee=ue))});const G={slots:I,slotProps:{list:d,transition:x,paper:m,...L}},te=dE({elementType:I.root,externalSlotProps:L.root,ownerState:O,className:[F.root,l]}),[Q,ie]=_o("paper",{className:F.paper,elementType:Zvr,externalForwardedProps:G,shouldForwardComponentProp:!0,ownerState:O}),[se,de]=_o("list",{className:_i(F.list,d.className),elementType:Xvr,shouldForwardComponentProp:!0,externalForwardedProps:G,getSlotProps:we=>({...we,onKeyDown:ue=>{Z(ue),we.onKeyDown?.(ue)}}),ownerState:O}),ne=typeof G.slotProps.transition=="function"?G.slotProps.transition(O):G.slotProps.transition;return k.jsx(Yvr,{onClose:h,anchorOrigin:{vertical:"bottom",horizontal:M?"right":"left"},transformOrigin:M?qvr:Gvr,slots:{root:I.root,paper:Q,backdrop:I.backdrop,...I.transition&&{transition:I.transition}},slotProps:{root:te,paper:ie,backdrop:typeof L.backdrop=="function"?L.backdrop(O):L.backdrop,transition:{...ne,onEntering:(...we)=>{q(...we),ne?.onEntering?.(...we)}}},open:p,ref:t,transitionDuration:w,ownerState:O,...A,classes:b,children:k.jsx(se,{actions:W,autoFocus:r&&(ee===-1||c),autoFocusItem:j,variant:T,...de,children:o})})});function Qvr(n){return No("MuiMenuItem",n)}const Hxe=Po("MuiMenuItem",["root","focusVisible","dense","disabled","divider","gutters","selected"]),Jvr=(n,e)=>{const{ownerState:t}=n;return[e.root,t.dense&&e.dense,t.divider&&e.divider,!t.disableGutters&&e.gutters]},ewr=n=>{const{disabled:e,dense:t,divider:i,disableGutters:r,selected:o,classes:l}=n,d=Fo({root:["root",t&&"dense",e&&"disabled",!r&&"gutters",i&&"divider",o&&"selected"]},Qvr,l);return{...l,...d}},twr=tn(D3,{shouldForwardProp:n=>B_(n)||n==="classes",name:"MuiMenuItem",slot:"Root",overridesResolver:Jvr})(Gs(({theme:n})=>({...n.typography.body1,display:"flex",justifyContent:"flex-start",alignItems:"center",position:"relative",textDecoration:"none",minHeight:48,paddingTop:6,paddingBottom:6,boxSizing:"border-box",whiteSpace:"nowrap","&:hover":{textDecoration:"none",backgroundColor:(n.vars||n).palette.action.hover,"@media (hover: none)":{backgroundColor:"transparent"}},[`&.${Hxe.selected}`]:{backgroundColor:n.alpha((n.vars||n).palette.primary.main,(n.vars||n).palette.action.selectedOpacity),[`&.${Hxe.focusVisible}`]:{backgroundColor:n.alpha((n.vars||n).palette.primary.main,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.focusOpacity}`)}},[`&.${Hxe.selected}:hover`]:{backgroundColor:n.alpha((n.vars||n).palette.primary.main,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.hoverOpacity}`),"@media (hover: none)":{backgroundColor:n.alpha((n.vars||n).palette.primary.main,(n.vars||n).palette.action.selectedOpacity)}},[`&.${Hxe.focusVisible}`]:{backgroundColor:(n.vars||n).palette.action.focus},[`&.${Hxe.disabled}`]:{opacity:(n.vars||n).palette.action.disabledOpacity},[`& + .${VPn.root}`]:{marginTop:n.spacing(1),marginBottom:n.spacing(1)},[`& + .${VPn.inset}`]:{marginLeft:52},[`& .${Wfe.root}`]:{marginTop:0,marginBottom:0},[`& .${Wfe.inset}`]:{paddingLeft:36},[`& .${KPn.root}`]:{minWidth:36},variants:[{props:({ownerState:e})=>!e.disableGutters,style:{paddingLeft:16,paddingRight:16}},{props:({ownerState:e})=>e.divider,style:{borderBottom:`1px solid ${(n.vars||n).palette.divider}`,backgroundClip:"padding-box"}},{props:({ownerState:e})=>!e.dense,style:{[n.breakpoints.up("sm")]:{minHeight:"auto"}}},{props:({ownerState:e})=>e.dense,style:{minHeight:32,paddingTop:4,paddingBottom:4,...n.typography.body2,[`& .${KPn.root} svg`]:{fontSize:"1.25rem"}}}]}))),Sm=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiMenuItem"}),{autoFocus:r=!1,component:o="li",dense:l=!1,divider:c=!1,disableGutters:d=!1,focusVisibleClassName:h,role:p="menuitem",tabIndex:m,className:b,...w}=i,_=D.useContext(nM),x=D.useMemo(()=>({dense:l||_.dense||!1,disableGutters:d}),[_.dense,l,d]),T=D.useRef(null);IS(()=>{r&&T.current&&T.current.focus()},[r]);const I={...i,dense:x.dense,divider:c,disableGutters:d},L=ewr(i),A=xm(T,t);let M;return i.disabled||(M=m!==void 0?m:-1),k.jsx(nM.Provider,{value:x,children:k.jsx(twr,{ref:A,role:p,tabIndex:M,component:o,focusVisibleClassName:_i(L.focusVisible,h),className:_i(L.root,b),...w,ownerState:I,classes:L})})});function nwr(n){return No("MuiNativeSelect",n)}const gUt=Po("MuiNativeSelect",["root","select","multiple","filled","outlined","standard","disabled","icon","iconOpen","iconFilled","iconOutlined","iconStandard","nativeInput","error"]),iwr=n=>{const{classes:e,variant:t,disabled:i,multiple:r,open:o,error:l}=n,c={select:["select",t,i&&"disabled",r&&"multiple",l&&"error"],icon:["icon",`icon${ii(t)}`,o&&"iconOpen",i&&"disabled"]};return Fo(c,nwr,e)},kui=tn("select",{name:"MuiNativeSelect"})(({theme:n})=>({MozAppearance:"none",WebkitAppearance:"none",userSelect:"none",borderRadius:0,cursor:"pointer","&:focus":{borderRadius:0},[`&.${gUt.disabled}`]:{cursor:"default"},"&[multiple]":{height:"auto"},"&:not([multiple]) option, &:not([multiple]) optgroup":{backgroundColor:(n.vars||n).palette.background.paper},variants:[{props:({ownerState:e})=>e.variant!=="filled"&&e.variant!=="outlined",style:{"&&&":{paddingRight:24,minWidth:16}}},{props:{variant:"filled"},style:{"&&&":{paddingRight:32}}},{props:{variant:"outlined"},style:{borderRadius:(n.vars||n).shape.borderRadius,"&:focus":{borderRadius:(n.vars||n).shape.borderRadius},"&&&":{paddingRight:32}}}]})),rwr=tn(kui,{name:"MuiNativeSelect",slot:"Select",shouldForwardProp:B_,overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.select,e[t.variant],t.error&&e.error,{[`&.${gUt.multiple}`]:e.multiple}]}})({}),Tui=tn("svg",{name:"MuiNativeSelect"})(({theme:n})=>({position:"absolute",right:0,top:"calc(50% - .5em)",pointerEvents:"none",color:(n.vars||n).palette.action.active,[`&.${gUt.disabled}`]:{color:(n.vars||n).palette.action.disabled},variants:[{props:({ownerState:e})=>e.open,style:{transform:"rotate(180deg)"}},{props:{variant:"filled"},style:{right:7}},{props:{variant:"outlined"},style:{right:7}}]})),swr=tn(Tui,{name:"MuiNativeSelect",slot:"Icon",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.icon,t.variant&&e[`icon${ii(t.variant)}`],t.open&&e.iconOpen]}})({}),owr=D.forwardRef(function(e,t){const{className:i,disabled:r,error:o,IconComponent:l,inputRef:c,variant:d="standard",...h}=e,p={...e,disabled:r,variant:d,error:o},m=iwr(p);return k.jsxs(D.Fragment,{children:[k.jsx(rwr,{ownerState:p,className:_i(m.select,i),disabled:r,ref:c||t,...h}),e.multiple?null:k.jsx(swr,{as:l,ownerState:p,className:m.icon})]})});var JPn;const awr=tn("fieldset",{name:"MuiNotchedOutlined",shouldForwardProp:B_})({textAlign:"left",position:"absolute",bottom:0,right:0,top:-5,left:0,margin:0,padding:"0 8px",pointerEvents:"none",borderRadius:"inherit",borderStyle:"solid",borderWidth:1,overflow:"hidden",minWidth:"0%"}),lwr=tn("legend",{name:"MuiNotchedOutlined",shouldForwardProp:B_})(Gs(({theme:n})=>({float:"unset",width:"auto",overflow:"hidden",variants:[{props:({ownerState:e})=>!e.withLabel,style:{padding:0,lineHeight:"11px",transition:n.transitions.create("width",{duration:150,easing:n.transitions.easing.easeOut})}},{props:({ownerState:e})=>e.withLabel,style:{display:"block",padding:0,height:11,fontSize:"0.75em",visibility:"hidden",maxWidth:.01,transition:n.transitions.create("max-width",{duration:50,easing:n.transitions.easing.easeOut}),whiteSpace:"nowrap","& > span":{paddingLeft:5,paddingRight:5,display:"inline-block",opacity:0,visibility:"visible"}}},{props:({ownerState:e})=>e.withLabel&&e.notched,style:{maxWidth:"100%",transition:n.transitions.create("max-width",{duration:100,easing:n.transitions.easing.easeOut,delay:50})}}]})));function cwr(n){const{children:e,classes:t,className:i,label:r,notched:o,...l}=n,c=r!=null&&r!=="",d={...n,notched:o,withLabel:c};return k.jsx(awr,{"aria-hidden":!0,className:i,ownerState:d,...l,children:k.jsx(lwr,{ownerState:d,children:c?k.jsx("span",{children:r}):JPn||(JPn=k.jsx("span",{className:"notranslate","aria-hidden":!0,children:"​"}))})})}const uwr=n=>{const{classes:e}=n,i=Fo({root:["root"],notchedOutline:["notchedOutline"],input:["input"]},u1r,e);return{...e,...i}},dwr=tn(qet,{shouldForwardProp:n=>B_(n)||n==="classes",name:"MuiOutlinedInput",slot:"Root",overridesResolver:zet})(Gs(({theme:n})=>{const e=n.palette.mode==="light"?"rgba(0, 0, 0, 0.23)":"rgba(255, 255, 255, 0.23)";return{position:"relative",borderRadius:(n.vars||n).shape.borderRadius,[`&:hover .${zD.notchedOutline}`]:{borderColor:(n.vars||n).palette.text.primary},"@media (hover: none)":{[`&:hover .${zD.notchedOutline}`]:{borderColor:n.vars?n.alpha(n.vars.palette.common.onBackground,.23):e}},[`&.${zD.focused} .${zD.notchedOutline}`]:{borderWidth:2},variants:[...Object.entries(n.palette).filter(Vh()).map(([t])=>({props:{color:t},style:{[`&.${zD.focused} .${zD.notchedOutline}`]:{borderColor:(n.vars||n).palette[t].main}}})),{props:{},style:{[`&.${zD.error} .${zD.notchedOutline}`]:{borderColor:(n.vars||n).palette.error.main},[`&.${zD.disabled} .${zD.notchedOutline}`]:{borderColor:(n.vars||n).palette.action.disabled}}},{props:({ownerState:t})=>t.startAdornment,style:{paddingLeft:14}},{props:({ownerState:t})=>t.endAdornment,style:{paddingRight:14}},{props:({ownerState:t})=>t.multiline,style:{padding:"16.5px 14px"}},{props:({ownerState:t,size:i})=>t.multiline&&i==="small",style:{padding:"8.5px 14px"}}]}})),hwr=tn(cwr,{name:"MuiOutlinedInput",slot:"NotchedOutline"})(Gs(({theme:n})=>{const e=n.palette.mode==="light"?"rgba(0, 0, 0, 0.23)":"rgba(255, 255, 255, 0.23)";return{borderColor:n.vars?n.alpha(n.vars.palette.common.onBackground,.23):e}})),fwr=tn(Get,{name:"MuiOutlinedInput",slot:"Input",overridesResolver:Uet})(Gs(({theme:n})=>({padding:"16.5px 14px",...!n.vars&&{"&:-webkit-autofill":{WebkitBoxShadow:n.palette.mode==="light"?null:"0 0 0 100px #266798 inset",WebkitTextFillColor:n.palette.mode==="light"?null:"#fff",caretColor:n.palette.mode==="light"?null:"#fff",borderRadius:"inherit"}},...n.vars&&{"&:-webkit-autofill":{borderRadius:"inherit"},[n.getColorSchemeSelector("dark")]:{"&:-webkit-autofill":{WebkitBoxShadow:"0 0 0 100px #266798 inset",WebkitTextFillColor:"#fff",caretColor:"#fff"}}},variants:[{props:{size:"small"},style:{padding:"8.5px 14px"}},{props:({ownerState:e})=>e.multiline,style:{padding:0}},{props:({ownerState:e})=>e.startAdornment,style:{paddingLeft:0}},{props:({ownerState:e})=>e.endAdornment,style:{paddingRight:0}}]}))),mUt=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiOutlinedInput"}),{components:r={},fullWidth:o=!1,inputComponent:l="input",label:c,multiline:d=!1,notched:h,slots:p={},slotProps:m={},type:b="text",...w}=i,_=uwr(i),x=DM(),T=jY({props:i,muiFormControl:x,states:["color","disabled","error","focused","hiddenLabel","size","required"]}),I={...i,color:T.color||"primary",disabled:T.disabled,error:T.error,focused:T.focused,formControl:x,fullWidth:o,hiddenLabel:T.hiddenLabel,multiline:d,size:T.size,type:b},L=p.root??r.Root??dwr,A=p.input??r.Input??fwr,[M,O]=_o("notchedOutline",{elementType:hwr,className:_.notchedOutline,shouldForwardComponentProp:!0,ownerState:I,externalForwardedProps:{slots:p,slotProps:m},additionalProps:{label:c!=null&&c!==""&&T.required?k.jsxs(D.Fragment,{children:[c," ","*"]}):c}});return k.jsx(Y1e,{slots:{root:L,input:A},slotProps:m,renderSuffix:F=>k.jsx(M,{...O,notched:typeof h<"u"?h:!!(F.startAdornment||F.filled||F.focused)}),fullWidth:o,inputComponent:l,multiline:d,ref:t,type:b,...w,classes:{..._,notchedOutline:null}})});mUt.muiName="Input";const pwr=Ya(k.jsx("path",{d:"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"})),gwr=Ya(k.jsx("path",{d:"M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"})),mwr=tn("span",{name:"MuiRadioButtonIcon",shouldForwardProp:B_})({position:"relative",display:"flex"}),bwr=tn(pwr,{name:"MuiRadioButtonIcon"})({transform:"scale(1)"}),vwr=tn(gwr,{name:"MuiRadioButtonIcon"})(Gs(({theme:n})=>({left:0,position:"absolute",transform:"scale(0)",transition:n.transitions.create("transform",{easing:n.transitions.easing.easeIn,duration:n.transitions.duration.shortest}),variants:[{props:{checked:!0},style:{transform:"scale(1)",transition:n.transitions.create("transform",{easing:n.transitions.easing.easeOut,duration:n.transitions.duration.shortest})}}]})));function Lui(n){const{checked:e=!1,classes:t={},fontSize:i}=n,r={...n,checked:e};return k.jsxs(mwr,{className:t.root,ownerState:r,children:[k.jsx(bwr,{fontSize:i,className:t.background,ownerState:r}),k.jsx(vwr,{fontSize:i,className:t.dot,ownerState:r})]})}const Dui=D.createContext(void 0);function wwr(){return D.useContext(Dui)}function ywr(n){return No("MuiRadio",n)}const e9n=Po("MuiRadio",["root","checked","disabled","colorPrimary","colorSecondary","sizeSmall"]),_wr=n=>{const{classes:e,color:t,size:i}=n,r={root:["root",`color${ii(t)}`,i!=="medium"&&`size${ii(i)}`]};return{...e,...Fo(r,ywr,e)}},Cwr=tn(cUt,{shouldForwardProp:n=>B_(n)||n==="classes",name:"MuiRadio",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.size!=="medium"&&e[`size${ii(t.size)}`],e[`color${ii(t.color)}`]]}})(Gs(({theme:n})=>({color:(n.vars||n).palette.text.secondary,[`&.${e9n.disabled}`]:{color:(n.vars||n).palette.action.disabled},variants:[{props:{color:"default",disabled:!1,disableRipple:!1},style:{"&:hover":{backgroundColor:n.alpha((n.vars||n).palette.action.active,(n.vars||n).palette.action.hoverOpacity)}}},...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e,disabled:!1,disableRipple:!1},style:{"&:hover":{backgroundColor:n.alpha((n.vars||n).palette[e].main,(n.vars||n).palette.action.hoverOpacity)}}})),...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e,disabled:!1},style:{[`&.${e9n.checked}`]:{color:(n.vars||n).palette[e].main}}})),{props:{disableRipple:!1},style:{"&:hover":{"@media (hover: none)":{backgroundColor:"transparent"}}}}]})));function Swr(n,e){return typeof e=="object"&&e!==null?n===e:String(n)===String(e)}const xwr=k.jsx(Lui,{checked:!0}),Ewr=k.jsx(Lui,{}),Zet=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiRadio"}),{checked:r,checkedIcon:o=xwr,color:l="primary",icon:c=Ewr,name:d,onChange:h,size:p="medium",className:m,disabled:b,disableRipple:w=!1,slots:_={},slotProps:x={},inputProps:T,...I}=i,L=DM();let A=b;L&&typeof A>"u"&&(A=L.disabled),A??=!1;const M={...i,disabled:A,disableRipple:w,color:l,size:p},O=_wr(M),F=wwr();let j=r;const W=zOt(h,F&&F.onChange);let q=d;F&&(typeof j>"u"&&(j=Swr(F.value,i.value)),typeof q>"u"&&(q=F.name));const Z=x.input??T,[ee,G]=_o("root",{ref:t,elementType:Cwr,className:_i(O.root,m),shouldForwardComponentProp:!0,externalForwardedProps:{slots:_,slotProps:x,...I},getSlotProps:te=>({...te,onChange:(Q,...ie)=>{te.onChange?.(Q,...ie),W(Q,...ie)}}),ownerState:M,additionalProps:{type:"radio",icon:D.cloneElement(c,{fontSize:c.props.fontSize??p}),checkedIcon:D.cloneElement(o,{fontSize:o.props.fontSize??p}),disabled:A,name:q,checked:j,slots:_,slotProps:{input:typeof Z=="function"?Z(M):Z}}});return k.jsx(ee,{...G,classes:O})});function kwr(n){return No("MuiRadioGroup",n)}Po("MuiRadioGroup",["root","row","error"]);const Twr=n=>{const{classes:e,row:t,error:i}=n;return Fo({root:["root",t&&"row",i&&"error"]},kwr,e)},bUt=D.forwardRef(function(e,t){const{actions:i,children:r,className:o,defaultValue:l,name:c,onChange:d,value:h,...p}=e,m=D.useRef(null),b=Twr(e),[w,_]=S9({controlled:h,default:l,name:"RadioGroup"});D.useImperativeHandle(i,()=>({focus:()=>{let L=m.current.querySelector("input:not(:disabled):checked");L||(L=m.current.querySelector("input:not(:disabled)")),L&&L.focus()}}),[]);const x=xm(t,m),T=YW(c),I=D.useMemo(()=>({name:T,onChange(L){_(L.target.value),d&&d(L,L.target.value)},value:w}),[T,d,_,w]);return k.jsx(Dui.Provider,{value:I,children:k.jsx(Hbr,{role:"radiogroup",ref:x,className:_i(b.root,o),...p,children:r})})}),Lwr={border:0,clip:"rect(0 0 0 0)",height:"1px",margin:"-1px",overflow:"hidden",padding:0,position:"absolute",whiteSpace:"nowrap",width:"1px"};function Iui(n){return No("MuiSelect",n)}const Bxe=Po("MuiSelect",["root","select","multiple","filled","outlined","standard","disabled","focused","icon","iconOpen","iconFilled","iconOutlined","iconStandard","nativeInput","error"]);var t9n;const Dwr=tn(kui,{name:"MuiSelect",slot:"Select",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[{[`&.${Bxe.select}`]:e.select},{[`&.${Bxe.select}`]:e[t.variant]},{[`&.${Bxe.error}`]:e.error},{[`&.${Bxe.multiple}`]:e.multiple}]}})({[`&.${Bxe.select}`]:{height:"auto",minHeight:"1.4375em",textOverflow:"ellipsis",whiteSpace:"nowrap",overflow:"hidden"}}),Iwr=tn(Tui,{name:"MuiSelect",slot:"Icon",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.icon,t.variant&&e[`icon${ii(t.variant)}`],t.open&&e.iconOpen]}})({}),Awr=tn("input",{shouldForwardProp:n=>jet(n)&&n!=="classes",name:"MuiSelect",slot:"NativeInput"})({bottom:0,left:0,position:"absolute",opacity:0,pointerEvents:"none",width:"100%",boxSizing:"border-box"});function n9n(n,e){return typeof e=="object"&&e!==null?n===e:String(n)===String(e)}function Rwr(n){return n==null||typeof n=="string"&&!n.trim()}const Mwr=n=>{const{classes:e,variant:t,disabled:i,multiple:r,open:o,error:l}=n,c={select:["select",t,i&&"disabled",r&&"multiple",l&&"error"],icon:["icon",`icon${ii(t)}`,o&&"iconOpen",i&&"disabled"],nativeInput:["nativeInput"]};return Fo(c,Iui,e)},Owr=D.forwardRef(function(e,t){const{"aria-describedby":i,"aria-label":r,autoFocus:o,autoWidth:l,children:c,className:d,defaultOpen:h,defaultValue:p,disabled:m,displayEmpty:b,error:w=!1,IconComponent:_,inputRef:x,labelId:T,MenuProps:I={},multiple:L,name:A,onBlur:M,onChange:O,onClose:F,onFocus:j,onKeyDown:W,onMouseDown:q,onOpen:Z,open:ee,readOnly:G,renderValue:te,required:Q,SelectDisplayProps:ie={},tabIndex:se,type:de,value:ne,variant:we="standard",...ue}=e,[ce,ye]=S9({controlled:ne,default:p,name:"Select"}),[he,pe]=S9({controlled:ee,default:h,name:"Select"}),me=D.useRef(null),be=D.useRef(null),[xe,Te]=D.useState(null),{current:Ge}=D.useRef(ee!=null),[tt,Ue]=D.useState(),Me=xm(t,x),He=D.useCallback(xt=>{be.current=xt,xt&&Te(xt)},[]),at=xe?.parentNode;D.useImperativeHandle(Me,()=>({focus:()=>{be.current.focus()},node:me.current,value:ce}),[ce]);const rt=xe!==null&&he;D.useEffect(()=>{if(!rt||!at||l||typeof ResizeObserver>"u")return;const xt=new ResizeObserver(()=>{Ue(at.clientWidth)});return xt.observe(at),()=>{xt.disconnect()}},[rt,at,l]),D.useEffect(()=>{h&&he&&xe&&!Ge&&(Ue(l?null:at.clientWidth),be.current.focus())},[xe,l]),D.useEffect(()=>{o&&be.current.focus()},[o]),D.useEffect(()=>{if(!T)return;const xt=hv(be.current).getElementById(T);if(xt){const Ei=()=>{getSelection().isCollapsed&&be.current.focus()};return xt.addEventListener("click",Ei),()=>{xt.removeEventListener("click",Ei)}}},[T]);const Be=(xt,Ei)=>{xt?Z&&Z(Ei):F&&F(Ei),Ge||(Ue(l?null:at.clientWidth),pe(xt))},lt=xt=>{q?.(xt),xt.button===0&&(xt.preventDefault(),be.current.focus(),Be(!0,xt))},ct=xt=>{Be(!1,xt)},ze=D.Children.toArray(c),Ke=xt=>{const Ei=ze.find(gr=>gr.props.value===xt.target.value);Ei!==void 0&&(ye(Ei.props.value),O&&O(xt,Ei))},$e=xt=>Ei=>{let gr;if(Ei.currentTarget.hasAttribute("tabindex")){if(L){gr=Array.isArray(ce)?ce.slice():[];const ss=ce.indexOf(xt.props.value);ss===-1?gr.push(xt.props.value):gr.splice(ss,1)}else gr=xt.props.value;if(xt.props.onClick&&xt.props.onClick(Ei),ce!==gr&&(ye(gr),O)){const ss=Ei.nativeEvent||Ei,us=new ss.constructor(ss.type,ss);Object.defineProperty(us,"target",{writable:!0,value:{value:gr,name:A}}),O(us,xt)}L||Be(!1,Ei)}},nt=xt=>{G||([" ","ArrowUp","ArrowDown","Enter"].includes(xt.key)&&(xt.preventDefault(),Be(!0,xt)),W?.(xt))},vt=xt=>{!rt&&M&&(Object.defineProperty(xt,"target",{writable:!0,value:{value:ce,name:A}}),M(xt))};delete ue["aria-invalid"];let Pt,Ct;const Ye=[];let wt=!1;(oGe({value:ce})||b)&&(te?Pt=te(ce):wt=!0);const zt=ze.map(xt=>{if(!D.isValidElement(xt))return null;let Ei;if(L){if(!Array.isArray(ce))throw new Error(bW(2));Ei=ce.some(gr=>n9n(gr,xt.props.value)),Ei&&wt&&Ye.push(xt.props.children)}else Ei=n9n(ce,xt.props.value),Ei&&wt&&(Ct=xt.props.children);return D.cloneElement(xt,{"aria-selected":Ei?"true":"false",onClick:$e(xt),onKeyUp:gr=>{gr.key===" "&&gr.preventDefault(),xt.props.onKeyUp&&xt.props.onKeyUp(gr)},role:"option",selected:Ei,value:void 0,"data-value":xt.props.value})});wt&&(L?Ye.length===0?Pt=null:Pt=Ye.reduce((xt,Ei,gr)=>(xt.push(Ei),gr{const{classes:e}=n,i=Fo({root:["root"]},Iui,e);return{...e,...i}},vUt={name:"MuiSelect",slot:"Root",shouldForwardProp:n=>B_(n)&&n!=="variant"},Pwr=tn(dUt,vUt)(""),Fwr=tn(mUt,vUt)(""),jwr=tn(uUt,vUt)(""),XW=D.forwardRef(function(e,t){const i=Wo({name:"MuiSelect",props:e}),{autoWidth:r=!1,children:o,classes:l={},className:c,defaultOpen:d=!1,displayEmpty:h=!1,IconComponent:p=dui,id:m,input:b,inputProps:w,label:_,labelId:x,MenuProps:T,multiple:I=!1,native:L=!1,onClose:A,onOpen:M,open:O,renderValue:F,SelectDisplayProps:j,variant:W="outlined",...q}=i,Z=L?owr:Owr,ee=DM(),G=jY({props:i,muiFormControl:ee,states:["variant","error"]}),te=G.variant||W,Q={...i,variant:te,classes:l},ie=Nwr(Q),{root:se,...de}=ie,ne=b||{standard:k.jsx(Pwr,{ownerState:Q}),outlined:k.jsx(Fwr,{label:_,ownerState:Q}),filled:k.jsx(jwr,{ownerState:Q})}[te],we=xm(t,FY(ne));return k.jsx(D.Fragment,{children:D.cloneElement(ne,{inputComponent:Z,inputProps:{children:o,error:G.error,IconComponent:p,variant:te,type:void 0,multiple:I,...L?{id:m}:{autoWidth:r,defaultOpen:d,displayEmpty:h,labelId:x,MenuProps:T,onClose:A,onOpen:M,open:O,renderValue:F,SelectDisplayProps:{id:m,...j}},...w,classes:w?O_(de,w.classes):de,...b?b.props.inputProps:{}},...(I&&L||h)&&te==="outlined"?{notched:!0}:{},ref:we,className:_i(ne.props.className,c,ie.root),...!b&&{variant:te},...q})})});XW.muiName="Select";function Hwr(n,e,t=(i,r)=>i===r){return n.length===e.length&&n.every((i,r)=>t(i,e[r]))}const Bwr=2;function che(n,e,t,i,r){return t===1?Math.min(n+e,r):Math.max(n-e,i)}function Aui(n,e){return n-e}function i9n(n,e){const{index:t}=n.reduce((i,r,o)=>{const l=Math.abs(e-r);return i===null||l({left:`${n}%`}),leap:n=>({width:`${n}%`})},"horizontal-reverse":{offset:n=>({right:`${n}%`}),leap:n=>({width:`${n}%`})},vertical:{offset:n=>({bottom:`${n}%`}),leap:n=>({height:`${n}%`})}},Uwr=n=>n;let WBe;function s9n(){return WBe===void 0&&(typeof CSS<"u"&&typeof CSS.supports=="function"?WBe=CSS.supports("touch-action","none"):WBe=!0),WBe}function qwr(n){const{"aria-labelledby":e,defaultValue:t,disabled:i=!1,disableSwap:r=!1,isRtl:o=!1,marks:l=!1,max:c=100,min:d=0,name:h,onChange:p,onChangeCommitted:m,orientation:b="horizontal",rootRef:w,scale:_=Uwr,step:x=1,shiftStep:T=10,tabIndex:I,value:L}=n,A=D.useRef(void 0),[M,O]=D.useState(-1),[F,j]=D.useState(-1),[W,q]=D.useState(!1),Z=D.useRef(0),ee=D.useRef(null),[G,te]=S9({controlled:L,default:t??d,name:"Slider"}),Q=p&&((Ct,Ye,wt)=>{const zt=Ct.nativeEvent||Ct,mn=new zt.constructor(zt.type,zt);Object.defineProperty(mn,"target",{writable:!0,value:{value:Ye,name:h}}),ee.current=Ye,p(mn,Ye,wt)}),ie=Array.isArray(G);let se=ie?G.slice().sort(Aui):[G];se=se.map(Ct=>Ct==null?d:ffe(Ct,d,c));const de=l===!0&&x!==null?[...Array(Math.floor((c-d)/x)+1)].map((Ct,Ye)=>({value:d+x*Ye})):l||[],ne=de.map(Ct=>Ct.value),[we,ue]=D.useState(-1),ce=D.useRef(null),ye=xm(w,ce),he=Ct=>Ye=>{const wt=Number(Ye.currentTarget.getAttribute("data-index"));JK(Ye.target)&&ue(wt),j(wt),Ct?.onFocus?.(Ye)},pe=Ct=>Ye=>{JK(Ye.target)||ue(-1),j(-1),Ct?.onBlur?.(Ye)},me=(Ct,Ye)=>{const wt=Number(Ct.currentTarget.getAttribute("data-index")),zt=se[wt],mn=ne.indexOf(zt);let xn=Ye;if(de&&x==null){const hn=ne[ne.length-1];xn>=hn?xn=hn:xn<=ne[0]?xn=ne[0]:xn=xnYe=>{if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","PageUp","PageDown","Home","End"].includes(Ye.key)){Ye.preventDefault();const wt=Number(Ye.currentTarget.getAttribute("data-index")),zt=se[wt];let mn=null;if(x!=null){const xn=Ye.shiftKey?T:x;switch(Ye.key){case"ArrowUp":mn=che(zt,xn,1,d,c);break;case"ArrowRight":mn=che(zt,xn,o?-1:1,d,c);break;case"ArrowDown":mn=che(zt,xn,-1,d,c);break;case"ArrowLeft":mn=che(zt,xn,o?1:-1,d,c);break;case"PageUp":mn=che(zt,T,1,d,c);break;case"PageDown":mn=che(zt,T,-1,d,c);break;case"Home":mn=d;break;case"End":mn=c;break}}else if(de){const xn=ne[ne.length-1],hn=ne.indexOf(zt),Zi=[o?"ArrowRight":"ArrowLeft","ArrowDown","PageDown","Home"],$i=[o?"ArrowLeft":"ArrowRight","ArrowUp","PageUp","End"];Zi.includes(Ye.key)?hn===0?mn=ne[0]:mn=ne[hn-1]:$i.includes(Ye.key)&&(hn===ne.length-1?mn=xn:mn=ne[hn+1])}mn!=null&&me(Ye,mn)}Ct?.onKeyDown?.(Ye)};IS(()=>{i&&ce.current.contains(document.activeElement)&&document.activeElement?.blur()},[i]),i&&M!==-1&&O(-1),i&&we!==-1&&ue(-1);const xe=Ct=>Ye=>{Ct.onChange?.(Ye),me(Ye,Ye.target.valueAsNumber)},Te=D.useRef(void 0);let Ge=b;o&&b==="horizontal"&&(Ge+="-reverse");const tt=({finger:Ct,move:Ye=!1})=>{const{current:wt}=ce,{width:zt,height:mn,bottom:xn,left:hn}=wt.getBoundingClientRect();let Zi;Ge.startsWith("vertical")?Zi=(xn-Ct.y)/mn:Zi=(Ct.x-hn)/zt,Ge.includes("-reverse")&&(Zi=1-Zi);let $i;if($i=Wwr(Zi,d,c),x)$i=$wr($i,x,d);else{const ps=i9n(ne,$i);$i=ne[ps]}$i=ffe($i,d,c);let Dr=0;if(ie){Ye?Dr=Te.current:Dr=i9n(se,$i),r&&($i=ffe($i,se[Dr-1]||-1/0,se[Dr+1]||1/0));const ps=$i;$i=r9n({values:se,newValue:$i,index:Dr}),r&&Ye||(Dr=$i.indexOf(ps),Te.current=Dr)}return{newValue:$i,activeIndex:Dr}},Ue=ub(Ct=>{const Ye=jBe(Ct,A);if(!Ye)return;if(Z.current+=1,Ct.type==="mousemove"&&Ct.buttons===0){Me(Ct);return}const{newValue:wt,activeIndex:zt}=tt({finger:Ye,move:!0});HBe({sliderRef:ce,activeIndex:zt,setActive:O}),te(wt),!W&&Z.current>Bwr&&q(!0),Q&&!BBe(wt,G)&&Q(Ct,wt,zt)}),Me=ub(Ct=>{const Ye=jBe(Ct,A);if(q(!1),!Ye)return;const{newValue:wt}=tt({finger:Ye,move:!0});O(-1),Ct.type==="touchend"&&j(-1),m&&m(Ct,ee.current??wt),A.current=void 0,at()}),He=ub(Ct=>{if(i)return;s9n()||Ct.preventDefault();const Ye=Ct.changedTouches[0];Ye!=null&&(A.current=Ye.identifier);const wt=jBe(Ct,A);if(wt!==!1){const{newValue:mn,activeIndex:xn}=tt({finger:wt});HBe({sliderRef:ce,activeIndex:xn,setActive:O}),te(mn),Q&&!BBe(mn,G)&&Q(Ct,mn,xn)}Z.current=0;const zt=hv(ce.current);zt.addEventListener("touchmove",Ue,{passive:!0}),zt.addEventListener("touchend",Me,{passive:!0})}),at=D.useCallback(()=>{const Ct=hv(ce.current);Ct.removeEventListener("mousemove",Ue),Ct.removeEventListener("mouseup",Me),Ct.removeEventListener("touchmove",Ue),Ct.removeEventListener("touchend",Me)},[Me,Ue]);D.useEffect(()=>{const{current:Ct}=ce;return Ct.addEventListener("touchstart",He,{passive:s9n()}),()=>{Ct.removeEventListener("touchstart",He),at()}},[at,He]),D.useEffect(()=>{i&&at()},[i,at]);const rt=Ct=>Ye=>{if(Ct.onMouseDown?.(Ye),i||Ye.defaultPrevented||Ye.button!==0)return;Ye.preventDefault();const wt=jBe(Ye,A);if(wt!==!1){const{newValue:mn,activeIndex:xn}=tt({finger:wt});HBe({sliderRef:ce,activeIndex:xn,setActive:O}),te(mn),Q&&!BBe(mn,G)&&Q(Ye,mn,xn)}Z.current=0;const zt=hv(ce.current);zt.addEventListener("mousemove",Ue,{passive:!0}),zt.addEventListener("mouseup",Me)},Be=lGe(ie?se[0]:d,d,c),lt=lGe(se[se.length-1],d,c)-Be,ct=(Ct={})=>{const Ye=vie(Ct),wt={onMouseDown:rt(Ye||{})},zt={...Ye,...wt};return{...Ct,ref:ye,...zt}},ze=Ct=>Ye=>{Ct.onMouseOver?.(Ye);const wt=Number(Ye.currentTarget.getAttribute("data-index"));j(wt)},Ke=Ct=>Ye=>{Ct.onMouseLeave?.(Ye),j(-1)},$e=(Ct={})=>{const Ye=vie(Ct),wt={onMouseOver:ze(Ye||{}),onMouseLeave:Ke(Ye||{})};return{...Ct,...Ye,...wt}},nt=Ct=>({pointerEvents:M!==-1&&M!==Ct?"none":void 0});let vt;return b==="vertical"&&(vt=o?"vertical-rl":"vertical-lr"),{active:M,axis:Ge,axisProps:zwr,dragging:W,focusedThumbIndex:we,getHiddenInputProps:(Ct={})=>{const Ye=vie(Ct),wt={onChange:xe(Ye||{}),onFocus:he(Ye||{}),onBlur:pe(Ye||{}),onKeyDown:be(Ye||{})},zt={...Ye,...wt};return{tabIndex:I,"aria-labelledby":e,"aria-orientation":b,"aria-valuemax":_(c),"aria-valuemin":_(d),name:h,type:"range",min:n.min,max:n.max,step:n.step===null&&n.marks?"any":n.step??void 0,disabled:i,...Ct,...zt,style:{...Lwr,direction:o?"rtl":"ltr",width:"100%",height:"100%",writingMode:vt}}},getRootProps:ct,getThumbProps:$e,marks:de,open:F,range:ie,rootRef:ye,trackLeap:lt,trackOffset:Be,values:se,getThumbStyle:nt}}const Gwr=n=>!n||!x9(n);function Kwr(n){return No("MuiSlider",n)}const f3=Po("MuiSlider",["root","active","colorPrimary","colorSecondary","colorError","colorInfo","colorSuccess","colorWarning","disabled","dragging","focusVisible","mark","markActive","marked","markLabel","markLabelActive","rail","sizeSmall","thumb","thumbColorPrimary","thumbColorSecondary","thumbColorError","thumbColorSuccess","thumbColorInfo","thumbColorWarning","track","trackInverted","trackFalse","thumbSizeSmall","valueLabel","valueLabelOpen","valueLabelCircle","valueLabelLabel","vertical"]),Ywr=n=>{const{open:e}=n;return{offset:_i(e&&f3.valueLabelOpen),circle:f3.valueLabelCircle,label:f3.valueLabelLabel}};function Zwr(n){const{children:e,className:t,value:i}=n,r=Ywr(n);return e?D.cloneElement(e,{className:e.props.className},k.jsxs(D.Fragment,{children:[e.props.children,k.jsx("span",{className:_i(r.offset,t),"aria-hidden":!0,children:k.jsx("span",{className:r.circle,children:k.jsx("span",{className:r.label,children:i})})})]})):null}function o9n(n){return n}const Xwr=tn("span",{name:"MuiSlider",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[`color${ii(t.color)}`],t.size!=="medium"&&e[`size${ii(t.size)}`],t.marked&&e.marked,t.orientation==="vertical"&&e.vertical,t.track==="inverted"&&e.trackInverted,t.track===!1&&e.trackFalse]}})(Gs(({theme:n})=>({borderRadius:12,boxSizing:"content-box",display:"inline-block",position:"relative",cursor:"pointer",touchAction:"none",WebkitTapHighlightColor:"transparent","@media print":{colorAdjust:"exact"},[`&.${f3.disabled}`]:{pointerEvents:"none",cursor:"default",color:(n.vars||n).palette.grey[400]},[`&.${f3.dragging}`]:{[`& .${f3.thumb}, & .${f3.track}`]:{transition:"none"}},variants:[...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{color:(n.vars||n).palette[e].main}})),{props:{orientation:"horizontal"},style:{height:4,width:"100%",padding:"13px 0","@media (pointer: coarse)":{padding:"20px 0"}}},{props:{orientation:"horizontal",size:"small"},style:{height:2}},{props:{orientation:"horizontal",marked:!0},style:{marginBottom:20}},{props:{orientation:"vertical"},style:{height:"100%",width:4,padding:"0 13px","@media (pointer: coarse)":{padding:"0 20px"}}},{props:{orientation:"vertical",size:"small"},style:{width:2}},{props:{orientation:"vertical",marked:!0},style:{marginRight:44}}]}))),Qwr=tn("span",{name:"MuiSlider",slot:"Rail"})({display:"block",position:"absolute",borderRadius:"inherit",backgroundColor:"currentColor",opacity:.38,variants:[{props:{orientation:"horizontal"},style:{width:"100%",height:"inherit",top:"50%",transform:"translateY(-50%)"}},{props:{orientation:"vertical"},style:{height:"100%",width:"inherit",left:"50%",transform:"translateX(-50%)"}},{props:{track:"inverted"},style:{opacity:1}}]}),Jwr=tn("span",{name:"MuiSlider",slot:"Track"})(Gs(({theme:n})=>({display:"block",position:"absolute",borderRadius:"inherit",border:"1px solid currentColor",backgroundColor:"currentColor",transition:n.transitions.create(["left","width","bottom","height"],{duration:n.transitions.duration.shortest}),variants:[{props:{size:"small"},style:{border:"none"}},{props:{orientation:"horizontal"},style:{height:"inherit",top:"50%",transform:"translateY(-50%)"}},{props:{orientation:"vertical"},style:{width:"inherit",left:"50%",transform:"translateX(-50%)"}},{props:{track:!1},style:{display:"none"}},...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e,track:"inverted"},style:{...n.vars?{backgroundColor:n.vars.palette.Slider[`${e}Track`],borderColor:n.vars.palette.Slider[`${e}Track`]}:{backgroundColor:n.lighten(n.palette[e].main,.62),borderColor:n.lighten(n.palette[e].main,.62),...n.applyStyles("dark",{backgroundColor:n.darken(n.palette[e].main,.5)}),...n.applyStyles("dark",{borderColor:n.darken(n.palette[e].main,.5)})}}}))]}))),eyr=tn("span",{name:"MuiSlider",slot:"Thumb",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.thumb,e[`thumbColor${ii(t.color)}`],t.size!=="medium"&&e[`thumbSize${ii(t.size)}`]]}})(Gs(({theme:n})=>({position:"absolute",width:20,height:20,boxSizing:"border-box",borderRadius:"50%",outline:0,backgroundColor:"currentColor",display:"flex",alignItems:"center",justifyContent:"center",transition:n.transitions.create(["box-shadow","left","bottom"],{duration:n.transitions.duration.shortest}),"&::before":{position:"absolute",content:'""',borderRadius:"inherit",width:"100%",height:"100%",boxShadow:(n.vars||n).shadows[2]},"&::after":{position:"absolute",content:'""',borderRadius:"50%",width:42,height:42,top:"50%",left:"50%",transform:"translate(-50%, -50%)"},[`&.${f3.disabled}`]:{"&:hover":{boxShadow:"none"}},variants:[{props:{size:"small"},style:{width:12,height:12,"&::before":{boxShadow:"none"}}},{props:{orientation:"horizontal"},style:{top:"50%",transform:"translate(-50%, -50%)"}},{props:{orientation:"vertical"},style:{left:"50%",transform:"translate(-50%, 50%)"}},...Object.entries(n.palette).filter(Vh()).map(([e])=>({props:{color:e},style:{[`&:hover, &.${f3.focusVisible}`]:{boxShadow:`0px 0px 0px 8px ${n.alpha((n.vars||n).palette[e].main,.16)}`,"@media (hover: none)":{boxShadow:"none"}},[`&.${f3.active}`]:{boxShadow:`0px 0px 0px 14px ${n.alpha((n.vars||n).palette[e].main,.16)}`}}}))]}))),tyr=tn(Zwr,{name:"MuiSlider",slot:"ValueLabel"})(Gs(({theme:n})=>({zIndex:1,whiteSpace:"nowrap",...n.typography.body2,fontWeight:500,transition:n.transitions.create(["transform"],{duration:n.transitions.duration.shortest}),position:"absolute",backgroundColor:(n.vars||n).palette.grey[600],borderRadius:2,color:(n.vars||n).palette.common.white,display:"flex",alignItems:"center",justifyContent:"center",padding:"0.25rem 0.75rem",variants:[{props:{orientation:"horizontal"},style:{transform:"translateY(-100%) scale(0)",top:"-10px",transformOrigin:"bottom center","&::before":{position:"absolute",content:'""',width:8,height:8,transform:"translate(-50%, 50%) rotate(45deg)",backgroundColor:"inherit",bottom:0,left:"50%"},[`&.${f3.valueLabelOpen}`]:{transform:"translateY(-100%) scale(1)"}}},{props:{orientation:"vertical"},style:{transform:"translateY(-50%) scale(0)",right:"30px",top:"50%",transformOrigin:"right center","&::before":{position:"absolute",content:'""',width:8,height:8,transform:"translate(-50%, -50%) rotate(45deg)",backgroundColor:"inherit",right:-8,top:"50%"},[`&.${f3.valueLabelOpen}`]:{transform:"translateY(-50%) scale(1)"}}},{props:{size:"small"},style:{fontSize:n.typography.pxToRem(12),padding:"0.25rem 0.5rem"}},{props:{orientation:"vertical",size:"small"},style:{right:"20px"}}]}))),nyr=tn("span",{name:"MuiSlider",slot:"Mark",shouldForwardProp:n=>jet(n)&&n!=="markActive",overridesResolver:(n,e)=>{const{markActive:t}=n;return[e.mark,t&&e.markActive]}})(Gs(({theme:n})=>({position:"absolute",width:2,height:2,borderRadius:1,backgroundColor:"currentColor",variants:[{props:{orientation:"horizontal"},style:{top:"50%",transform:"translate(-1px, -50%)"}},{props:{orientation:"vertical"},style:{left:"50%",transform:"translate(-50%, 1px)"}},{props:{markActive:!0},style:{backgroundColor:(n.vars||n).palette.background.paper,opacity:.8}}]}))),iyr=tn("span",{name:"MuiSlider",slot:"MarkLabel",shouldForwardProp:n=>jet(n)&&n!=="markLabelActive"})(Gs(({theme:n})=>({...n.typography.body2,color:(n.vars||n).palette.text.secondary,position:"absolute",whiteSpace:"nowrap",variants:[{props:{orientation:"horizontal"},style:{top:30,transform:"translateX(-50%)","@media (pointer: coarse)":{top:40}}},{props:{orientation:"vertical"},style:{left:36,transform:"translateY(50%)","@media (pointer: coarse)":{left:44}}},{props:{markLabelActive:!0},style:{color:(n.vars||n).palette.text.primary}}]}))),ryr=n=>{const{disabled:e,dragging:t,marked:i,orientation:r,track:o,classes:l,color:c,size:d}=n,h={root:["root",e&&"disabled",t&&"dragging",i&&"marked",r==="vertical"&&"vertical",o==="inverted"&&"trackInverted",o===!1&&"trackFalse",c&&`color${ii(c)}`,d&&`size${ii(d)}`],rail:["rail"],track:["track"],mark:["mark"],markActive:["markActive"],markLabel:["markLabel"],markLabelActive:["markLabelActive"],valueLabel:["valueLabel"],thumb:["thumb",e&&"disabled",d&&`thumbSize${ii(d)}`,c&&`thumbColor${ii(c)}`],active:["active"],disabled:["disabled"],focusVisible:["focusVisible"]};return Fo(h,Kwr,l)},syr=({children:n})=>n,oyr=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiSlider"}),r=MY(),{"aria-label":o,"aria-valuetext":l,"aria-labelledby":c,component:d="span",components:h={},componentsProps:p={},color:m="primary",classes:b,className:w,disableSwap:_=!1,disabled:x=!1,getAriaLabel:T,getAriaValueText:I,marks:L=!1,max:A=100,min:M=0,name:O,onChange:F,onChangeCommitted:j,orientation:W="horizontal",shiftStep:q=10,size:Z="medium",step:ee=1,scale:G=o9n,slotProps:te,slots:Q,tabIndex:ie,track:se="normal",value:de,valueLabelDisplay:ne="off",valueLabelFormat:we=o9n,...ue}=i,ce={...i,isRtl:r,max:A,min:M,classes:b,disabled:x,disableSwap:_,orientation:W,marks:L,color:m,size:Z,step:ee,shiftStep:q,scale:G,track:se,valueLabelDisplay:ne,valueLabelFormat:we},{axisProps:ye,getRootProps:he,getHiddenInputProps:pe,getThumbProps:me,open:be,active:xe,axis:Te,focusedThumbIndex:Ge,range:tt,dragging:Ue,marks:Me,values:He,trackOffset:at,trackLeap:rt,getThumbStyle:Be}=qwr({...ce,rootRef:t});ce.marked=Me.length>0&&Me.some(_r=>_r.label),ce.dragging=Ue,ce.focusedThumbIndex=Ge;const lt=ryr(ce),ct=Q?.root??h.Root??Xwr,ze=Q?.rail??h.Rail??Qwr,Ke=Q?.track??h.Track??Jwr,$e=Q?.thumb??h.Thumb??eyr,nt=Q?.valueLabel??h.ValueLabel??tyr,vt=Q?.mark??h.Mark??nyr,Pt=Q?.markLabel??h.MarkLabel??iyr,Ct=Q?.input??h.Input??"input",Ye=te?.root??p.root,wt=te?.rail??p.rail,zt=te?.track??p.track,mn=te?.thumb??p.thumb,xn=te?.valueLabel??p.valueLabel,hn=te?.mark??p.mark,Zi=te?.markLabel??p.markLabel,$i=te?.input??p.input,Dr=dE({elementType:ct,getSlotProps:he,externalSlotProps:Ye,externalForwardedProps:ue,additionalProps:{...Gwr(ct)&&{as:d}},ownerState:{...ce,...Ye?.ownerState},className:[lt.root,w]}),ps=dE({elementType:ze,externalSlotProps:wt,ownerState:ce,className:lt.rail}),nn=dE({elementType:Ke,externalSlotProps:zt,additionalProps:{style:{...ye[Te].offset(at),...ye[Te].leap(rt)}},ownerState:{...ce,...zt?.ownerState},className:lt.track}),xt=dE({elementType:$e,getSlotProps:me,externalSlotProps:mn,ownerState:{...ce,...mn?.ownerState},className:lt.thumb}),Ei=dE({elementType:nt,externalSlotProps:xn,ownerState:{...ce,...xn?.ownerState},className:lt.valueLabel}),gr=dE({elementType:vt,externalSlotProps:hn,ownerState:ce,className:lt.mark}),ss=dE({elementType:Pt,externalSlotProps:Zi,ownerState:ce,className:lt.markLabel}),us=dE({elementType:Ct,getSlotProps:pe,externalSlotProps:$i,ownerState:ce});return k.jsxs(ct,{...Dr,children:[k.jsx(ze,{...ps}),k.jsx(Ke,{...nn}),Me.filter(_r=>_r.value>=M&&_r.value<=A).map((_r,uo)=>{const xs=lGe(_r.value,M,A),Fs=ye[Te].offset(xs);let eo;return se===!1?eo=He.includes(_r.value):eo=se==="normal"&&(tt?_r.value>=He[0]&&_r.value<=He[He.length-1]:_r.value<=He[0])||se==="inverted"&&(tt?_r.value<=He[0]||_r.value>=He[He.length-1]:_r.value>=He[0]),k.jsxs(D.Fragment,{children:[k.jsx(vt,{"data-index":uo,...gr,...!x9(vt)&&{markActive:eo},style:{...Fs,...gr.style},className:_i(gr.className,eo&<.markActive)}),_r.label!=null?k.jsx(Pt,{"aria-hidden":!0,"data-index":uo,...ss,...!x9(Pt)&&{markLabelActive:eo},style:{...Fs,...ss.style},className:_i(lt.markLabel,ss.className,eo&<.markLabelActive),children:_r.label}):null]},uo)}),He.map((_r,uo)=>{const xs=lGe(_r,M,A),Fs=ye[Te].offset(xs),eo=ne==="off"?syr:nt;return k.jsx(eo,{...!x9(eo)&&{valueLabelFormat:we,valueLabelDisplay:ne,value:typeof we=="function"?we(G(_r),uo):we,index:uo,open:be===uo||xe===uo||ne==="on",disabled:x},...Ei,children:k.jsx($e,{"data-index":uo,...xt,className:_i(lt.thumb,xt.className,xe===uo&<.active,Ge===uo&<.focusVisible),style:{...Fs,...Be(uo),...xt.style},children:k.jsx(Ct,{"data-index":uo,"aria-label":T?T(uo):o,"aria-valuenow":G(_r),"aria-labelledby":c,"aria-valuetext":I?I(G(_r),uo):l,value:He[uo],...us})})},uo)})]})});function ayr(n={}){const{autoHideDuration:e=null,disableWindowBlurListener:t=!1,onClose:i,open:r,resumeHideDuration:o}=n,l=NG();D.useEffect(()=>{if(!r)return;function I(L){L.defaultPrevented||L.key==="Escape"&&i?.(L,"escapeKeyDown")}return document.addEventListener("keydown",I),()=>{document.removeEventListener("keydown",I)}},[r,i]);const c=ub((I,L)=>{i?.(I,L)}),d=ub(I=>{!i||I==null||l.start(I,()=>{c(null,"timeout")})});D.useEffect(()=>(r&&d(e),l.clear),[r,e,d,l]);const h=I=>{i?.(I,"clickaway")},p=l.clear,m=D.useCallback(()=>{e!=null&&d(o??e*.5)},[e,o,d]),b=I=>L=>{const A=I.onBlur;A?.(L),m()},w=I=>L=>{const A=I.onFocus;A?.(L),p()},_=I=>L=>{const A=I.onMouseEnter;A?.(L),p()},x=I=>L=>{const A=I.onMouseLeave;A?.(L),m()};return D.useEffect(()=>{if(!t&&r)return window.addEventListener("focus",m),window.addEventListener("blur",p),()=>{window.removeEventListener("focus",m),window.removeEventListener("blur",p)}},[t,r,m,p]),{getRootProps:(I={})=>{const L={...vie(n),...vie(I)};return{role:"presentation",...I,...L,onBlur:b(L),onFocus:w(L),onMouseEnter:_(L),onMouseLeave:x(L)}},onClickAway:h}}function lyr(n){return No("MuiSnackbarContent",n)}Po("MuiSnackbarContent",["root","message","action"]);const cyr=n=>{const{classes:e}=n;return Fo({root:["root"],action:["action"],message:["message"]},lyr,e)},uyr=tn(Jf,{name:"MuiSnackbarContent",slot:"Root"})(Gs(({theme:n})=>{const e=n.palette.mode==="light"?.8:.98;return{...n.typography.body2,color:n.vars?n.vars.palette.SnackbarContent.color:n.palette.getContrastText(lLe(n.palette.background.default,e)),backgroundColor:n.vars?n.vars.palette.SnackbarContent.bg:lLe(n.palette.background.default,e),display:"flex",alignItems:"center",flexWrap:"wrap",padding:"6px 16px",flexGrow:1,[n.breakpoints.up("sm")]:{flexGrow:"initial",minWidth:288}}})),dyr=tn("div",{name:"MuiSnackbarContent",slot:"Message"})({padding:"8px 0"}),hyr=tn("div",{name:"MuiSnackbarContent",slot:"Action"})({display:"flex",alignItems:"center",marginLeft:"auto",paddingLeft:16,marginRight:-8}),fyr=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiSnackbarContent"}),{action:r,className:o,message:l,role:c="alert",...d}=i,h=i,p=cyr(h);return k.jsxs(uyr,{role:c,elevation:6,className:_i(p.root,o),ownerState:h,ref:t,...d,children:[k.jsx(dyr,{className:p.message,ownerState:h,children:l}),r?k.jsx(hyr,{className:p.action,ownerState:h,children:r}):null]})});function pyr(n){return No("MuiSnackbar",n)}Po("MuiSnackbar",["root","anchorOriginTopCenter","anchorOriginBottomCenter","anchorOriginTopRight","anchorOriginBottomRight","anchorOriginTopLeft","anchorOriginBottomLeft"]);const gyr=n=>{const{classes:e,anchorOrigin:t}=n,i={root:["root",`anchorOrigin${ii(t.vertical)}${ii(t.horizontal)}`]};return Fo(i,pyr,e)},myr=tn("div",{name:"MuiSnackbar",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[`anchorOrigin${ii(t.anchorOrigin.vertical)}${ii(t.anchorOrigin.horizontal)}`]]}})(Gs(({theme:n})=>({zIndex:(n.vars||n).zIndex.snackbar,position:"fixed",display:"flex",left:8,right:8,justifyContent:"center",alignItems:"center",variants:[{props:({ownerState:e})=>e.anchorOrigin.vertical==="top",style:{top:8,[n.breakpoints.up("sm")]:{top:24}}},{props:({ownerState:e})=>e.anchorOrigin.vertical!=="top",style:{bottom:8,[n.breakpoints.up("sm")]:{bottom:24}}},{props:({ownerState:e})=>e.anchorOrigin.horizontal==="left",style:{justifyContent:"flex-start",[n.breakpoints.up("sm")]:{left:24,right:"auto"}}},{props:({ownerState:e})=>e.anchorOrigin.horizontal==="right",style:{justifyContent:"flex-end",[n.breakpoints.up("sm")]:{right:24,left:"auto"}}},{props:({ownerState:e})=>e.anchorOrigin.horizontal==="center",style:{[n.breakpoints.up("sm")]:{left:"50%",right:"auto",transform:"translateX(-50%)"}}}]}))),Rui=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiSnackbar"}),r=Lf(),o={enter:r.transitions.duration.enteringScreen,exit:r.transitions.duration.leavingScreen},{action:l,anchorOrigin:{vertical:c,horizontal:d}={vertical:"bottom",horizontal:"left"},autoHideDuration:h=null,children:p,className:m,ClickAwayListenerProps:b,ContentProps:w,disableWindowBlurListener:_=!1,message:x,onBlur:T,onClose:I,onFocus:L,onMouseEnter:A,onMouseLeave:M,open:O,resumeHideDuration:F,slots:j={},slotProps:W={},TransitionComponent:q,transitionDuration:Z=o,TransitionProps:{onEnter:ee,onExited:G,...te}={},...Q}=i,ie={...i,anchorOrigin:{vertical:c,horizontal:d},autoHideDuration:h,disableWindowBlurListener:_,TransitionComponent:q,transitionDuration:Z},se=gyr(ie),{getRootProps:de,onClickAway:ne}=ayr(ie),[we,ue]=D.useState(!0),ce=He=>{ue(!0),G&&G(He)},ye=(He,at)=>{ue(!1),ee&&ee(He,at)},he={slots:{transition:q,...j},slotProps:{content:w,clickAwayListener:b,transition:te,...W}},[pe,me]=_o("root",{ref:t,className:[se.root,m],elementType:myr,getSlotProps:de,externalForwardedProps:{...he,...Q},ownerState:ie}),[be,{ownerState:xe,...Te}]=_o("clickAwayListener",{elementType:HY,externalForwardedProps:he,getSlotProps:He=>({onClickAway:(...at)=>{const rt=at[0];He.onClickAway?.(...at),!rt?.defaultMuiPrevented&&ne(...at)}}),ownerState:ie}),[Ge,tt]=_o("content",{elementType:fyr,shouldForwardComponentProp:!0,externalForwardedProps:he,additionalProps:{message:x,action:l},ownerState:ie}),[Ue,Me]=_o("transition",{elementType:yW,externalForwardedProps:he,getSlotProps:He=>({onEnter:(...at)=>{He.onEnter?.(...at),ye(...at)},onExited:(...at)=>{He.onExited?.(...at),ce(...at)}}),additionalProps:{appear:!0,in:O,timeout:Z,direction:c==="top"?"down":"up"},ownerState:ie});return!O&&we?null:k.jsx(be,{...Te,...j.clickAwayListener&&{ownerState:xe},children:k.jsx(pe,{...me,children:k.jsx(Ue,{...Me,children:p||k.jsx(Ge,{...tt})})})})});function byr(n){return No("MuiTooltip",n)}const rb=Po("MuiTooltip",["popper","popperInteractive","popperArrow","popperClose","tooltip","tooltipArrow","touch","tooltipPlacementLeft","tooltipPlacementRight","tooltipPlacementTop","tooltipPlacementBottom","arrow"]);function vyr(n){return Math.round(n*1e5)/1e5}const wyr=n=>{const{classes:e,disableInteractive:t,arrow:i,touch:r,placement:o}=n,l={popper:["popper",!t&&"popperInteractive",i&&"popperArrow"],tooltip:["tooltip",i&&"tooltipArrow",r&&"touch",`tooltipPlacement${ii(o.split("-")[0])}`],arrow:["arrow"]};return Fo(l,byr,e)},yyr=tn(c8,{name:"MuiTooltip",slot:"Popper",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.popper,!t.disableInteractive&&e.popperInteractive,t.arrow&&e.popperArrow,!t.open&&e.popperClose]}})(Gs(({theme:n})=>({zIndex:(n.vars||n).zIndex.tooltip,pointerEvents:"none",variants:[{props:({ownerState:e})=>!e.disableInteractive,style:{pointerEvents:"auto"}},{props:({open:e})=>!e,style:{pointerEvents:"none"}},{props:({ownerState:e})=>e.arrow,style:{[`&[data-popper-placement*="bottom"] .${rb.arrow}`]:{top:0,marginTop:"-0.71em","&::before":{transformOrigin:"0 100%"}},[`&[data-popper-placement*="top"] .${rb.arrow}`]:{bottom:0,marginBottom:"-0.71em","&::before":{transformOrigin:"100% 0"}},[`&[data-popper-placement*="right"] .${rb.arrow}`]:{height:"1em",width:"0.71em","&::before":{transformOrigin:"100% 100%"}},[`&[data-popper-placement*="left"] .${rb.arrow}`]:{height:"1em",width:"0.71em","&::before":{transformOrigin:"0 0"}}}},{props:({ownerState:e})=>e.arrow&&!e.isRtl,style:{[`&[data-popper-placement*="right"] .${rb.arrow}`]:{left:0,marginLeft:"-0.71em"}}},{props:({ownerState:e})=>e.arrow&&!!e.isRtl,style:{[`&[data-popper-placement*="right"] .${rb.arrow}`]:{right:0,marginRight:"-0.71em"}}},{props:({ownerState:e})=>e.arrow&&!e.isRtl,style:{[`&[data-popper-placement*="left"] .${rb.arrow}`]:{right:0,marginRight:"-0.71em"}}},{props:({ownerState:e})=>e.arrow&&!!e.isRtl,style:{[`&[data-popper-placement*="left"] .${rb.arrow}`]:{left:0,marginLeft:"-0.71em"}}}]}))),_yr=tn("div",{name:"MuiTooltip",slot:"Tooltip",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.tooltip,t.touch&&e.touch,t.arrow&&e.tooltipArrow,e[`tooltipPlacement${ii(t.placement.split("-")[0])}`]]}})(Gs(({theme:n})=>({backgroundColor:n.vars?n.vars.palette.Tooltip.bg:n.alpha(n.palette.grey[700],.92),borderRadius:(n.vars||n).shape.borderRadius,color:(n.vars||n).palette.common.white,fontFamily:n.typography.fontFamily,padding:"4px 8px",fontSize:n.typography.pxToRem(11),maxWidth:300,margin:2,wordWrap:"break-word",fontWeight:n.typography.fontWeightMedium,[`.${rb.popper}[data-popper-placement*="left"] &`]:{transformOrigin:"right center"},[`.${rb.popper}[data-popper-placement*="right"] &`]:{transformOrigin:"left center"},[`.${rb.popper}[data-popper-placement*="top"] &`]:{transformOrigin:"center bottom",marginBottom:"14px"},[`.${rb.popper}[data-popper-placement*="bottom"] &`]:{transformOrigin:"center top",marginTop:"14px"},variants:[{props:({ownerState:e})=>e.arrow,style:{position:"relative",margin:0}},{props:({ownerState:e})=>e.touch,style:{padding:"8px 16px",fontSize:n.typography.pxToRem(14),lineHeight:`${vyr(16/14)}em`,fontWeight:n.typography.fontWeightRegular}},{props:({ownerState:e})=>!e.isRtl,style:{[`.${rb.popper}[data-popper-placement*="left"] &`]:{marginRight:"14px"},[`.${rb.popper}[data-popper-placement*="right"] &`]:{marginLeft:"14px"}}},{props:({ownerState:e})=>!e.isRtl&&e.touch,style:{[`.${rb.popper}[data-popper-placement*="left"] &`]:{marginRight:"24px"},[`.${rb.popper}[data-popper-placement*="right"] &`]:{marginLeft:"24px"}}},{props:({ownerState:e})=>!!e.isRtl,style:{[`.${rb.popper}[data-popper-placement*="left"] &`]:{marginLeft:"14px"},[`.${rb.popper}[data-popper-placement*="right"] &`]:{marginRight:"14px"}}},{props:({ownerState:e})=>!!e.isRtl&&e.touch,style:{[`.${rb.popper}[data-popper-placement*="left"] &`]:{marginLeft:"24px"},[`.${rb.popper}[data-popper-placement*="right"] &`]:{marginRight:"24px"}}},{props:({ownerState:e})=>e.touch,style:{[`.${rb.popper}[data-popper-placement*="top"] &`]:{marginBottom:"24px"}}},{props:({ownerState:e})=>e.touch,style:{[`.${rb.popper}[data-popper-placement*="bottom"] &`]:{marginTop:"24px"}}}]}))),Cyr=tn("span",{name:"MuiTooltip",slot:"Arrow"})(Gs(({theme:n})=>({overflow:"hidden",position:"absolute",width:"1em",height:"0.71em",boxSizing:"border-box",color:n.vars?n.vars.palette.Tooltip.bg:n.alpha(n.palette.grey[700],.9),"&::before":{content:'""',margin:"auto",display:"block",width:"100%",height:"100%",backgroundColor:"currentColor",transform:"rotate(45deg)"}})));let VBe=!1;const a9n=new Wet;let Wxe={x:0,y:0};function $Be(n,e){return(t,...i)=>{e&&e(t,...i),n(t,...i)}}const Jc=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiTooltip"}),{arrow:r=!1,children:o,classes:l,components:c={},componentsProps:d={},describeChild:h=!1,disableFocusListener:p=!1,disableHoverListener:m=!1,disableInteractive:b=!1,disableTouchListener:w=!1,enterDelay:_=100,enterNextDelay:x=0,enterTouchDelay:T=700,followCursor:I=!1,id:L,leaveDelay:A=0,leaveTouchDelay:M=1500,onClose:O,onOpen:F,open:j,placement:W="bottom",PopperComponent:q,PopperProps:Z={},slotProps:ee={},slots:G={},title:te,TransitionComponent:Q,TransitionProps:ie,...se}=i,de=D.isValidElement(o)?o:k.jsx("span",{children:o}),ne=Lf(),we=MY(),[ue,ce]=D.useState(),[ye,he]=D.useState(null),pe=D.useRef(!1),me=b||I,be=NG(),xe=NG(),Te=NG(),Ge=NG(),[tt,Ue]=S9({controlled:j,default:!1,name:"Tooltip",state:"open"});let Me=tt;const He=YW(L),at=D.useRef(),rt=ub(()=>{at.current!==void 0&&(document.body.style.WebkitUserSelect=at.current,at.current=void 0),Ge.clear()});D.useEffect(()=>rt,[rt]);const Be=Ri=>{a9n.clear(),VBe=!0,Ue(!0),F&&!Me&&F(Ri)},lt=ub(Ri=>{a9n.start(800+A,()=>{VBe=!1}),Ue(!1),O&&Me&&O(Ri),be.start(ne.transitions.duration.shortest,()=>{pe.current=!1})}),ct=Ri=>{pe.current&&Ri.type!=="touchstart"||(ue&&ue.removeAttribute("title"),xe.clear(),Te.clear(),_||VBe&&x?xe.start(VBe?x:_,()=>{Be(Ri)}):Be(Ri))},ze=Ri=>{xe.clear(),Te.start(A,()=>{lt(Ri)})},[,Ke]=D.useState(!1),$e=Ri=>{const Ls=Ri?.target??ue;if(!Ls||!JK(Ls)){Ke(!1);const Cr=Ri??new Event("blur");!Ri&&Ls&&(Object.defineProperty(Cr,"target",{value:Ls}),Object.defineProperty(Cr,"currentTarget",{value:Ls})),ze(Cr)}},nt=Ri=>{ue||ce(Ri.currentTarget),JK(Ri.target)&&(Ke(!0),ct(Ri))},vt=Ri=>{pe.current=!0;const Ls=de.props;Ls.onTouchStart&&Ls.onTouchStart(Ri)},Pt=Ri=>{vt(Ri),Te.clear(),be.clear(),rt(),at.current=document.body.style.WebkitUserSelect,document.body.style.WebkitUserSelect="none",Ge.start(T,()=>{document.body.style.WebkitUserSelect=at.current,ct(Ri)})},Ct=Ri=>{de.props.onTouchEnd&&de.props.onTouchEnd(Ri),rt(),Te.start(M,()=>{lt(Ri)})};D.useEffect(()=>{if(!Me)return;function Ri(Ls){Ls.key==="Escape"&<(Ls)}return document.addEventListener("keydown",Ri),()=>{document.removeEventListener("keydown",Ri)}},[lt,Me]);const Ye=xm(FY(de),ce,t);!te&&te!==0&&(Me=!1);const wt=D.useRef(),zt=Ri=>{const Ls=de.props;Ls.onMouseMove&&Ls.onMouseMove(Ri),Wxe={x:Ri.clientX,y:Ri.clientY},wt.current&&wt.current.update()},mn={},xn=typeof te=="string";h?(mn.title=!Me&&xn&&!m?te:null,mn["aria-describedby"]=Me?He:null):(mn["aria-label"]=xn?te:null,mn["aria-labelledby"]=Me&&!xn?He:null);const hn={...mn,...se,...de.props,className:_i(se.className,de.props.className),onTouchStart:vt,ref:Ye,...I?{onMouseMove:zt}:{}},Zi={};w||(hn.onTouchStart=Pt,hn.onTouchEnd=Ct),m||(hn.onMouseOver=$Be(ct,hn.onMouseOver),hn.onMouseLeave=$Be(ze,hn.onMouseLeave),me||(Zi.onMouseOver=ct,Zi.onMouseLeave=ze)),p||(hn.onFocus=$Be(nt,hn.onFocus),hn.onBlur=$Be($e,hn.onBlur),me||(Zi.onFocus=nt,Zi.onBlur=$e));const $i={...i,isRtl:we,arrow:r,disableInteractive:me,placement:W,PopperComponentProp:q,touch:pe.current},Dr=typeof ee.popper=="function"?ee.popper($i):ee.popper,ps=D.useMemo(()=>{let Ri=[{name:"arrow",enabled:!!ye,options:{element:ye,padding:4}}];return Z.popperOptions?.modifiers&&(Ri=Ri.concat(Z.popperOptions.modifiers)),Dr?.popperOptions?.modifiers&&(Ri=Ri.concat(Dr.popperOptions.modifiers)),{...Z.popperOptions,...Dr?.popperOptions,modifiers:Ri}},[ye,Z.popperOptions,Dr?.popperOptions]),nn=wyr($i),xt=typeof ee.transition=="function"?ee.transition($i):ee.transition,Ei={slots:{popper:c.Popper,transition:c.Transition??Q,tooltip:c.Tooltip,arrow:c.Arrow,...G},slotProps:{arrow:ee.arrow??d.arrow,popper:{...Z,...Dr??d.popper},tooltip:ee.tooltip??d.tooltip,transition:{...ie,...xt??d.transition}}},[gr,ss]=_o("popper",{elementType:yyr,externalForwardedProps:Ei,ownerState:$i,className:_i(nn.popper,Z?.className)}),[us,_r]=_o("transition",{elementType:yW,externalForwardedProps:Ei,ownerState:$i}),[uo,xs]=_o("tooltip",{elementType:_yr,className:nn.tooltip,externalForwardedProps:Ei,ownerState:$i}),[Fs,eo]=_o("arrow",{elementType:Cyr,className:nn.arrow,externalForwardedProps:Ei,ownerState:$i,ref:he});return k.jsxs(D.Fragment,{children:[D.cloneElement(de,hn),k.jsx(gr,{as:q??c8,placement:W,anchorEl:I?{getBoundingClientRect:()=>({top:Wxe.y,left:Wxe.x,right:Wxe.x,bottom:Wxe.y,width:0,height:0})}:ue,popperRef:wt,open:ue?Me:!1,id:He,transition:!0,...Zi,...ss,popperOptions:ps,children:({TransitionProps:Ri})=>k.jsx(us,{timeout:ne.transitions.duration.shorter,...Ri,..._r,children:k.jsxs(uo,{...xs,children:[te,r?k.jsx(Fs,{...eo}):null]})})})]})}),Ef=ahr({createStyledComponent:tn("div",{name:"MuiStack",slot:"Root"}),useThemeProps:n=>Wo({props:n,name:"MuiStack"})});function Syr(n){return No("MuiSwitch",n)}const rE=Po("MuiSwitch",["root","edgeStart","edgeEnd","switchBase","colorPrimary","colorSecondary","sizeSmall","sizeMedium","checked","disabled","input","thumb","track"]),xyr=n=>{const{classes:e,edge:t,size:i,color:r,checked:o,disabled:l}=n,c={root:["root",t&&`edge${ii(t)}`,`size${ii(i)}`],switchBase:["switchBase",`color${ii(r)}`,o&&"checked",l&&"disabled"],thumb:["thumb"],track:["track"],input:["input"]},d=Fo(c,Syr,e);return{...e,...d}},Eyr=tn("span",{name:"MuiSwitch",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.edge&&e[`edge${ii(t.edge)}`],e[`size${ii(t.size)}`]]}})({display:"inline-flex",width:58,height:38,overflow:"hidden",padding:12,boxSizing:"border-box",position:"relative",flexShrink:0,zIndex:0,verticalAlign:"middle","@media print":{colorAdjust:"exact"},variants:[{props:{edge:"start"},style:{marginLeft:-8}},{props:{edge:"end"},style:{marginRight:-8}},{props:{size:"small"},style:{width:40,height:24,padding:7,[`& .${rE.thumb}`]:{width:16,height:16},[`& .${rE.switchBase}`]:{padding:4,[`&.${rE.checked}`]:{transform:"translateX(16px)"}}}}]}),kyr=tn(cUt,{name:"MuiSwitch",slot:"SwitchBase",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.switchBase,{[`& .${rE.input}`]:e.input},t.color!=="default"&&e[`color${ii(t.color)}`]]}})(Gs(({theme:n})=>({position:"absolute",top:0,left:0,zIndex:1,color:n.vars?n.vars.palette.Switch.defaultColor:`${n.palette.mode==="light"?n.palette.common.white:n.palette.grey[300]}`,transition:n.transitions.create(["left","transform"],{duration:n.transitions.duration.shortest}),[`&.${rE.checked}`]:{transform:"translateX(20px)"},[`&.${rE.disabled}`]:{color:n.vars?n.vars.palette.Switch.defaultDisabledColor:`${n.palette.mode==="light"?n.palette.grey[100]:n.palette.grey[600]}`},[`&.${rE.checked} + .${rE.track}`]:{opacity:.5},[`&.${rE.disabled} + .${rE.track}`]:{opacity:n.vars?n.vars.opacity.switchTrackDisabled:`${n.palette.mode==="light"?.12:.2}`},[`& .${rE.input}`]:{left:"-100%",width:"300%"}})),Gs(({theme:n})=>({"&:hover":{backgroundColor:n.alpha((n.vars||n).palette.action.active,(n.vars||n).palette.action.hoverOpacity),"@media (hover: none)":{backgroundColor:"transparent"}},variants:[...Object.entries(n.palette).filter(Vh(["light"])).map(([e])=>({props:{color:e},style:{[`&.${rE.checked}`]:{color:(n.vars||n).palette[e].main,"&:hover":{backgroundColor:n.alpha((n.vars||n).palette[e].main,(n.vars||n).palette.action.hoverOpacity),"@media (hover: none)":{backgroundColor:"transparent"}},[`&.${rE.disabled}`]:{color:n.vars?n.vars.palette.Switch[`${e}DisabledColor`]:`${n.palette.mode==="light"?n.lighten(n.palette[e].main,.62):n.darken(n.palette[e].main,.55)}`}},[`&.${rE.checked} + .${rE.track}`]:{backgroundColor:(n.vars||n).palette[e].main}}}))]}))),Tyr=tn("span",{name:"MuiSwitch",slot:"Track"})(Gs(({theme:n})=>({height:"100%",width:"100%",borderRadius:14/2,zIndex:-1,transition:n.transitions.create(["opacity","background-color"],{duration:n.transitions.duration.shortest}),backgroundColor:n.vars?n.vars.palette.common.onBackground:`${n.palette.mode==="light"?n.palette.common.black:n.palette.common.white}`,opacity:n.vars?n.vars.opacity.switchTrack:`${n.palette.mode==="light"?.38:.3}`}))),Lyr=tn("span",{name:"MuiSwitch",slot:"Thumb"})(Gs(({theme:n})=>({boxShadow:(n.vars||n).shadows[1],backgroundColor:"currentColor",width:20,height:20,borderRadius:"50%"}))),mLe=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiSwitch"}),{className:r,color:o="primary",edge:l=!1,size:c="medium",sx:d,slots:h={},slotProps:p={},...m}=i,b={...i,color:o,edge:l,size:c},w=xyr(b),_={slots:h,slotProps:p},[x,T]=_o("root",{className:_i(w.root,r),elementType:Eyr,externalForwardedProps:_,ownerState:b,additionalProps:{sx:d}}),[I,L]=_o("thumb",{className:w.thumb,elementType:Lyr,externalForwardedProps:_,ownerState:b}),A=k.jsx(I,{...L}),[M,O]=_o("track",{className:w.track,elementType:Tyr,externalForwardedProps:_,ownerState:b});return k.jsxs(x,{...T,children:[k.jsx(kyr,{type:"checkbox",icon:A,checkedIcon:A,ref:t,ownerState:b,...m,classes:{...w,root:w.switchBase},slots:{...h.switchBase&&{root:h.switchBase},...h.input&&{input:h.input}},slotProps:{...p.switchBase&&{root:typeof p.switchBase=="function"?p.switchBase(b):p.switchBase},input:{role:"switch"},...p.input&&{input:typeof p.input=="function"?p.input(b):p.input}}}),k.jsx(M,{...O})]})});function Dyr(n){return No("MuiTab",n)}const PD=Po("MuiTab",["root","labelIcon","textColorInherit","textColorPrimary","textColorSecondary","selected","disabled","fullWidth","wrapped","iconWrapper","icon"]),Iyr=n=>{const{classes:e,textColor:t,fullWidth:i,wrapped:r,icon:o,label:l,selected:c,disabled:d}=n,h={root:["root",o&&l&&"labelIcon",`textColor${ii(t)}`,i&&"fullWidth",r&&"wrapped",c&&"selected",d&&"disabled"],icon:["iconWrapper","icon"]};return Fo(h,Dyr,e)},Ayr=tn(D3,{name:"MuiTab",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.label&&t.icon&&e.labelIcon,e[`textColor${ii(t.textColor)}`],t.fullWidth&&e.fullWidth,t.wrapped&&e.wrapped,{[`& .${PD.iconWrapper}`]:e.iconWrapper},{[`& .${PD.icon}`]:e.icon}]}})(Gs(({theme:n})=>({...n.typography.button,maxWidth:360,minWidth:90,position:"relative",minHeight:48,flexShrink:0,padding:"12px 16px",overflow:"hidden",whiteSpace:"normal",textAlign:"center",lineHeight:1.25,variants:[{props:({ownerState:e})=>e.label&&(e.iconPosition==="top"||e.iconPosition==="bottom"),style:{flexDirection:"column"}},{props:({ownerState:e})=>e.label&&e.iconPosition!=="top"&&e.iconPosition!=="bottom",style:{flexDirection:"row"}},{props:({ownerState:e})=>e.icon&&e.label,style:{minHeight:72,paddingTop:9,paddingBottom:9}},{props:({ownerState:e,iconPosition:t})=>e.icon&&e.label&&t==="top",style:{[`& > .${PD.icon}`]:{marginBottom:6}}},{props:({ownerState:e,iconPosition:t})=>e.icon&&e.label&&t==="bottom",style:{[`& > .${PD.icon}`]:{marginTop:6}}},{props:({ownerState:e,iconPosition:t})=>e.icon&&e.label&&t==="start",style:{[`& > .${PD.icon}`]:{marginRight:n.spacing(1)}}},{props:({ownerState:e,iconPosition:t})=>e.icon&&e.label&&t==="end",style:{[`& > .${PD.icon}`]:{marginLeft:n.spacing(1)}}},{props:{textColor:"inherit"},style:{color:"inherit",opacity:.6,[`&.${PD.selected}`]:{opacity:1},[`&.${PD.disabled}`]:{opacity:(n.vars||n).palette.action.disabledOpacity}}},{props:{textColor:"primary"},style:{color:(n.vars||n).palette.text.secondary,[`&.${PD.selected}`]:{color:(n.vars||n).palette.primary.main},[`&.${PD.disabled}`]:{color:(n.vars||n).palette.text.disabled}}},{props:{textColor:"secondary"},style:{color:(n.vars||n).palette.text.secondary,[`&.${PD.selected}`]:{color:(n.vars||n).palette.secondary.main},[`&.${PD.disabled}`]:{color:(n.vars||n).palette.text.disabled}}},{props:({ownerState:e})=>e.fullWidth,style:{flexShrink:1,flexGrow:1,flexBasis:0,maxWidth:"none"}},{props:({ownerState:e})=>e.wrapped,style:{fontSize:n.typography.pxToRem(12)}}]}))),DE=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiTab"}),{className:r,disabled:o=!1,disableFocusRipple:l=!1,fullWidth:c,icon:d,iconPosition:h="top",indicator:p,label:m,onChange:b,onClick:w,onFocus:_,selected:x,selectionFollowsFocus:T,textColor:I="inherit",value:L,wrapped:A=!1,...M}=i,O={...i,disabled:o,disableFocusRipple:l,selected:x,icon:!!d,iconPosition:h,label:!!m,fullWidth:c,textColor:I,wrapped:A},F=Iyr(O),j=d&&m&&D.isValidElement(d)?D.cloneElement(d,{className:_i(F.icon,d.props.className)}):d,W=Z=>{!x&&b&&b(Z,L),w&&w(Z)},q=Z=>{T&&!x&&b&&b(Z,L),_&&_(Z)};return k.jsxs(Ayr,{focusRipple:!l,className:_i(F.root,r),ref:t,role:"tab","aria-selected":x,disabled:o,onClick:W,onFocus:q,ownerState:O,tabIndex:x?0:-1,...M,children:[h==="top"||h==="start"?k.jsxs(D.Fragment,{children:[j,m]}):k.jsxs(D.Fragment,{children:[m,j]}),p]})}),Mui=D.createContext();function Ryr(n){return No("MuiTable",n)}Po("MuiTable",["root","stickyHeader"]);const Myr=n=>{const{classes:e,stickyHeader:t}=n;return Fo({root:["root",t&&"stickyHeader"]},Ryr,e)},Oyr=tn("table",{name:"MuiTable",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.stickyHeader&&e.stickyHeader]}})(Gs(({theme:n})=>({display:"table",width:"100%",borderCollapse:"collapse",borderSpacing:0,"& caption":{...n.typography.body2,padding:n.spacing(2),color:(n.vars||n).palette.text.secondary,textAlign:"left",captionSide:"bottom"},variants:[{props:({ownerState:e})=>e.stickyHeader,style:{borderCollapse:"separate"}}]}))),l9n="table",Nyr=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiTable"}),{className:r,component:o=l9n,padding:l="normal",size:c="medium",stickyHeader:d=!1,...h}=i,p={...i,component:o,padding:l,size:c,stickyHeader:d},m=Myr(p),b=D.useMemo(()=>({padding:l,size:c,stickyHeader:d}),[l,c,d]);return k.jsx(Mui.Provider,{value:b,children:k.jsx(Oyr,{as:o,role:o===l9n?null:"table",ref:t,className:_i(m.root,r),ownerState:p,...h})})}),wUt=D.createContext();function Pyr(n){return No("MuiTableBody",n)}Po("MuiTableBody",["root"]);const Fyr=n=>{const{classes:e}=n;return Fo({root:["root"]},Pyr,e)},jyr=tn("tbody",{name:"MuiTableBody",slot:"Root"})({display:"table-row-group"}),Hyr={variant:"body"},c9n="tbody",Byr=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiTableBody"}),{className:r,component:o=c9n,...l}=i,c={...i,component:o},d=Fyr(c);return k.jsx(wUt.Provider,{value:Hyr,children:k.jsx(jyr,{className:_i(d.root,r),as:o,ref:t,role:o===c9n?null:"rowgroup",ownerState:c,...l})})});function Wyr(n){return No("MuiTableCell",n)}const Vyr=Po("MuiTableCell",["root","head","body","footer","sizeSmall","sizeMedium","paddingCheckbox","paddingNone","alignLeft","alignCenter","alignRight","alignJustify","stickyHeader"]),$yr=n=>{const{classes:e,variant:t,align:i,padding:r,size:o,stickyHeader:l}=n,c={root:["root",t,l&&"stickyHeader",i!=="inherit"&&`align${ii(i)}`,r!=="normal"&&`padding${ii(r)}`,`size${ii(o)}`]};return Fo(c,Wyr,e)},zyr=tn("td",{name:"MuiTableCell",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,e[t.variant],e[`size${ii(t.size)}`],t.padding!=="normal"&&e[`padding${ii(t.padding)}`],t.align!=="inherit"&&e[`align${ii(t.align)}`],t.stickyHeader&&e.stickyHeader]}})(Gs(({theme:n})=>({...n.typography.body2,display:"table-cell",verticalAlign:"inherit",borderBottom:n.vars?`1px solid ${n.vars.palette.TableCell.border}`:`1px solid - ${n.palette.mode==="light"?n.lighten(n.alpha(n.palette.divider,1),.88):n.darken(n.alpha(n.palette.divider,1),.68)}`,textAlign:"left",padding:16,variants:[{props:{variant:"head"},style:{color:(n.vars||n).palette.text.primary,lineHeight:n.typography.pxToRem(24),fontWeight:n.typography.fontWeightMedium}},{props:{variant:"body"},style:{color:(n.vars||n).palette.text.primary}},{props:{variant:"footer"},style:{color:(n.vars||n).palette.text.secondary,lineHeight:n.typography.pxToRem(21),fontSize:n.typography.pxToRem(12)}},{props:{size:"small"},style:{padding:"6px 16px",[`&.${Vyr.paddingCheckbox}`]:{width:24,padding:"0 12px 0 16px","& > *":{padding:0}}}},{props:{padding:"checkbox"},style:{width:48,padding:"0 0 0 4px"}},{props:{padding:"none"},style:{padding:0}},{props:{align:"left"},style:{textAlign:"left"}},{props:{align:"center"},style:{textAlign:"center"}},{props:{align:"right"},style:{textAlign:"right",flexDirection:"row-reverse"}},{props:{align:"justify"},style:{textAlign:"justify"}},{props:({ownerState:e})=>e.stickyHeader,style:{position:"sticky",top:0,zIndex:2,backgroundColor:(n.vars||n).palette.background.default}}]}))),zBe=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiTableCell"}),{align:r="inherit",className:o,component:l,padding:c,scope:d,size:h,sortDirection:p,variant:m,...b}=i,w=D.useContext(Mui),_=D.useContext(wUt),x=_&&_.variant==="head";let T;l?T=l:T=x?"th":"td";let I=d;T==="td"?I=void 0:!I&&x&&(I="col");const L=m||_&&_.variant,A={...i,align:r,component:T,padding:c||(w&&w.padding?w.padding:"normal"),size:h||(w&&w.size?w.size:"medium"),sortDirection:p,stickyHeader:L==="head"&&w&&w.stickyHeader,variant:L},M=$yr(A);let O=null;return p&&(O=p==="asc"?"ascending":"descending"),k.jsx(zyr,{as:T,ref:t,className:_i(M.root,o),"aria-sort":O,scope:I,ownerState:A,...b})});function Uyr(n){return No("MuiToolbar",n)}Po("MuiToolbar",["root","gutters","regular","dense"]);const qyr=n=>{const{classes:e,disableGutters:t,variant:i}=n;return Fo({root:["root",!t&&"gutters",i]},Uyr,e)},Gyr=tn("div",{name:"MuiToolbar",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,!t.disableGutters&&e.gutters,e[t.variant]]}})(Gs(({theme:n})=>({position:"relative",display:"flex",alignItems:"center",variants:[{props:({ownerState:e})=>!e.disableGutters,style:{paddingLeft:n.spacing(2),paddingRight:n.spacing(2),[n.breakpoints.up("sm")]:{paddingLeft:n.spacing(3),paddingRight:n.spacing(3)}}},{props:{variant:"dense"},style:{minHeight:48}},{props:{variant:"regular"},style:n.mixins.toolbar}]}))),Kyr=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiToolbar"}),{className:r,component:o="div",disableGutters:l=!1,variant:c="regular",...d}=i,h={...i,component:o,disableGutters:l,variant:c},p=qyr(h);return k.jsx(Gyr,{as:o,className:_i(p.root,r),ref:t,ownerState:h,...d})}),Yyr=Ya(k.jsx("path",{d:"M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"})),Zyr=Ya(k.jsx("path",{d:"M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"}));function Xyr(n){return No("MuiTableRow",n)}const u9n=Po("MuiTableRow",["root","selected","hover","head","footer"]),Qyr=n=>{const{classes:e,selected:t,hover:i,head:r,footer:o}=n;return Fo({root:["root",t&&"selected",i&&"hover",r&&"head",o&&"footer"]},Xyr,e)},Jyr=tn("tr",{name:"MuiTableRow",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.head&&e.head,t.footer&&e.footer]}})(Gs(({theme:n})=>({color:"inherit",display:"table-row",verticalAlign:"middle",outline:0,[`&.${u9n.hover}:hover`]:{backgroundColor:(n.vars||n).palette.action.hover},[`&.${u9n.selected}`]:{backgroundColor:n.alpha((n.vars||n).palette.primary.main,(n.vars||n).palette.action.selectedOpacity),"&:hover":{backgroundColor:n.alpha((n.vars||n).palette.primary.main,`${(n.vars||n).palette.action.selectedOpacity} + ${(n.vars||n).palette.action.hoverOpacity}`)}}}))),d9n="tr",e_r=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiTableRow"}),{className:r,component:o=d9n,hover:l=!1,selected:c=!1,...d}=i,h=D.useContext(wUt),p={...i,component:o,hover:l,selected:c,head:h&&h.variant==="head",footer:h&&h.variant==="footer"},m=Qyr(p);return k.jsx(Jyr,{as:o,ref:t,className:_i(m.root,r),role:o===d9n?null:"row",ownerState:p,...d})});function t_r(n){return(1+Math.sin(Math.PI*n-Math.PI/2))/2}function n_r(n,e,t,i={},r=()=>{}){const{ease:o=t_r,duration:l=300}=i;let c=null;const d=e[n];let h=!1;const p=()=>{h=!0},m=b=>{if(h){r(new Error("Animation cancelled"));return}c===null&&(c=b);const w=Math.min(1,(b-c)/l);if(e[n]=o(w)*(t-d)+d,w>=1){requestAnimationFrame(()=>{r(null)});return}requestAnimationFrame(m)};return d===t?(r(new Error("Element already at target position")),p):(requestAnimationFrame(m),p)}const i_r={width:99,height:99,position:"absolute",top:-9999,overflow:"scroll"};function r_r(n){const{onChange:e,...t}=n,i=D.useRef(),r=D.useRef(null),o=()=>{i.current=r.current.offsetHeight-r.current.clientHeight};return IS(()=>{const l=g5e(()=>{const d=i.current;o(),d!==i.current&&e(i.current)}),c=Y6(r.current);return c.addEventListener("resize",l),()=>{l.clear(),c.removeEventListener("resize",l)}},[e]),D.useEffect(()=>{o(),e(i.current)},[e]),k.jsx("div",{style:i_r,...t,ref:r})}function s_r(n){return No("MuiTabScrollButton",n)}const o_r=Po("MuiTabScrollButton",["root","vertical","horizontal","disabled"]),a_r=n=>{const{classes:e,orientation:t,disabled:i}=n;return Fo({root:["root",t,i&&"disabled"]},s_r,e)},l_r=tn(D3,{name:"MuiTabScrollButton",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.root,t.orientation&&e[t.orientation]]}})({width:40,flexShrink:0,opacity:.8,[`&.${o_r.disabled}`]:{opacity:0},variants:[{props:{orientation:"vertical"},style:{width:"100%",height:40,"& svg":{transform:"var(--TabScrollButton-svgRotate)"}}}]}),c_r=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiTabScrollButton"}),{className:r,slots:o={},slotProps:l={},direction:c,orientation:d,disabled:h,...p}=i,m=MY(),b={isRtl:m,...i},w=a_r(b),_=o.StartScrollButtonIcon??Yyr,x=o.EndScrollButtonIcon??Zyr,T=dE({elementType:_,externalSlotProps:l.startScrollButtonIcon,additionalProps:{fontSize:"small"},ownerState:b}),I=dE({elementType:x,externalSlotProps:l.endScrollButtonIcon,additionalProps:{fontSize:"small"},ownerState:b});return k.jsx(l_r,{component:"div",className:_i(w.root,r),ref:t,role:null,ownerState:b,tabIndex:null,...p,style:{...p.style,...d==="vertical"&&{"--TabScrollButton-svgRotate":`rotate(${m?-90:90}deg)`}},children:c==="left"?k.jsx(_,{...T}):k.jsx(x,{...I})})});function u_r(n){return No("MuiTabs",n)}const yUe=Po("MuiTabs",["root","vertical","list","flexContainer","flexContainerVertical","centered","scroller","fixed","scrollableX","scrollableY","hideScrollbar","scrollButtons","scrollButtonsHideMobile","indicator"]),h9n=(n,e)=>n===e?n.firstChild:e&&e.nextElementSibling?e.nextElementSibling:n.firstChild,f9n=(n,e)=>n===e?n.lastChild:e&&e.previousElementSibling?e.previousElementSibling:n.lastChild,UBe=(n,e,t)=>{let i=!1,r=t(n,e);for(;r;){if(r===n.firstChild){if(i)return;i=!0}const o=r.disabled||r.getAttribute("aria-disabled")==="true";if(!r.hasAttribute("tabindex")||o)r=t(n,r);else{r.focus();return}}},d_r=n=>{const{vertical:e,fixed:t,hideScrollbar:i,scrollableX:r,scrollableY:o,centered:l,scrollButtonsHideMobile:c,classes:d}=n;return Fo({root:["root",e&&"vertical"],scroller:["scroller",t&&"fixed",i&&"hideScrollbar",r&&"scrollableX",o&&"scrollableY"],list:["list","flexContainer",e&&"flexContainerVertical",e&&"vertical",l&&"centered"],indicator:["indicator"],scrollButtons:["scrollButtons",c&&"scrollButtonsHideMobile"],scrollableX:[r&&"scrollableX"],hideScrollbar:[i&&"hideScrollbar"]},u_r,d)},h_r=tn("div",{name:"MuiTabs",slot:"Root",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[{[`& .${yUe.scrollButtons}`]:e.scrollButtons},{[`& .${yUe.scrollButtons}`]:t.scrollButtonsHideMobile&&e.scrollButtonsHideMobile},e.root,t.vertical&&e.vertical]}})(Gs(({theme:n})=>({overflow:"hidden",minHeight:48,WebkitOverflowScrolling:"touch",display:"flex",variants:[{props:({ownerState:e})=>e.vertical,style:{flexDirection:"column"}},{props:({ownerState:e})=>e.scrollButtonsHideMobile,style:{[`& .${yUe.scrollButtons}`]:{[n.breakpoints.down("sm")]:{display:"none"}}}}]}))),f_r=tn("div",{name:"MuiTabs",slot:"Scroller",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.scroller,t.fixed&&e.fixed,t.hideScrollbar&&e.hideScrollbar,t.scrollableX&&e.scrollableX,t.scrollableY&&e.scrollableY]}})({position:"relative",display:"inline-block",flex:"1 1 auto",whiteSpace:"nowrap",variants:[{props:({ownerState:n})=>n.fixed,style:{overflowX:"hidden",width:"100%"}},{props:({ownerState:n})=>n.hideScrollbar,style:{scrollbarWidth:"none","&::-webkit-scrollbar":{display:"none"}}},{props:({ownerState:n})=>n.scrollableX,style:{overflowX:"auto",overflowY:"hidden"}},{props:({ownerState:n})=>n.scrollableY,style:{overflowY:"auto",overflowX:"hidden"}}]}),p_r=tn("div",{name:"MuiTabs",slot:"List",overridesResolver:(n,e)=>{const{ownerState:t}=n;return[e.list,e.flexContainer,t.vertical&&e.flexContainerVertical,t.centered&&e.centered]}})({display:"flex",variants:[{props:({ownerState:n})=>n.vertical,style:{flexDirection:"column"}},{props:({ownerState:n})=>n.centered,style:{justifyContent:"center"}}]}),g_r=tn("span",{name:"MuiTabs",slot:"Indicator"})(Gs(({theme:n})=>({position:"absolute",height:2,bottom:0,width:"100%",transition:n.transitions.create(),variants:[{props:{indicatorColor:"primary"},style:{backgroundColor:(n.vars||n).palette.primary.main}},{props:{indicatorColor:"secondary"},style:{backgroundColor:(n.vars||n).palette.secondary.main}},{props:({ownerState:e})=>e.vertical,style:{height:"100%",width:2,right:0}}]}))),m_r=tn(r_r)({overflowX:"auto",overflowY:"hidden",scrollbarWidth:"none","&::-webkit-scrollbar":{display:"none"}}),p9n={},BY=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiTabs"}),r=Lf(),o=MY(),{"aria-label":l,"aria-labelledby":c,action:d,centered:h=!1,children:p,className:m,component:b="div",allowScrollButtonsMobile:w=!1,indicatorColor:_="primary",onChange:x,orientation:T="horizontal",ScrollButtonComponent:I,scrollButtons:L="auto",selectionFollowsFocus:A,slots:M={},slotProps:O={},TabIndicatorProps:F={},TabScrollButtonProps:j={},textColor:W="primary",value:q,variant:Z="standard",visibleScrollbar:ee=!1,...G}=i,te=Z==="scrollable",Q=T==="vertical",ie=Q?"scrollTop":"scrollLeft",se=Q?"top":"left",de=Q?"bottom":"right",ne=Q?"clientHeight":"clientWidth",we=Q?"height":"width",ue={...i,component:b,allowScrollButtonsMobile:w,indicatorColor:_,orientation:T,vertical:Q,scrollButtons:L,textColor:W,variant:Z,visibleScrollbar:ee,fixed:!te,hideScrollbar:te&&!ee,scrollableX:te&&!Q,scrollableY:te&&Q,centered:h&&!te,scrollButtonsHideMobile:!w},ce=d_r(ue),ye=dE({elementType:M.StartScrollButtonIcon,externalSlotProps:O.startScrollButtonIcon,ownerState:ue}),he=dE({elementType:M.EndScrollButtonIcon,externalSlotProps:O.endScrollButtonIcon,ownerState:ue}),[pe,me]=D.useState(!1),[be,xe]=D.useState(p9n),[Te,Ge]=D.useState(!1),[tt,Ue]=D.useState(!1),[Me,He]=D.useState(!1),[at,rt]=D.useState({overflow:"hidden",scrollbarWidth:0}),Be=new Map,lt=D.useRef(null),ct=D.useRef(null),ze={slots:M,slotProps:{indicator:F,scrollButtons:j,...O}},Ke=()=>{const Cr=lt.current;let Sr;if(Cr){const Ks=Cr.getBoundingClientRect();Sr={clientWidth:Cr.clientWidth,scrollLeft:Cr.scrollLeft,scrollTop:Cr.scrollTop,scrollWidth:Cr.scrollWidth,top:Ks.top,bottom:Ks.bottom,left:Ks.left,right:Ks.right}}let os;if(Cr&&q!==!1){const Ks=ct.current.children;if(Ks.length>0){const Ft=Ks[Be.get(q)];os=Ft?Ft.getBoundingClientRect():null}}return{tabsMeta:Sr,tabMeta:os}},$e=ub(()=>{const{tabsMeta:Cr,tabMeta:Sr}=Ke();let os=0,Ks;Q?(Ks="top",Sr&&Cr&&(os=Sr.top-Cr.top+Cr.scrollTop)):(Ks=o?"right":"left",Sr&&Cr&&(os=(o?-1:1)*(Sr[Ks]-Cr[Ks]+Cr.scrollLeft)));const Ft={[Ks]:os,[we]:Sr?Sr[we]:0};if(typeof be[Ks]!="number"||typeof be[we]!="number")xe(Ft);else{const rn=Math.abs(be[Ks]-Ft[Ks]),zn=Math.abs(be[we]-Ft[we]);(rn>=1||zn>=1)&&xe(Ft)}}),nt=(Cr,{animation:Sr=!0}={})=>{Sr?n_r(ie,lt.current,Cr,{duration:r.transitions.duration.standard}):lt.current[ie]=Cr},vt=Cr=>{let Sr=lt.current[ie];Q?Sr+=Cr:Sr+=Cr*(o?-1:1),nt(Sr)},Pt=()=>{const Cr=lt.current[ne];let Sr=0;const os=Array.from(ct.current.children);for(let Ks=0;KsCr){Ks===0&&(Sr=Cr);break}Sr+=Ft[ne]}return Sr},Ct=()=>{vt(-1*Pt())},Ye=()=>{vt(Pt())},[wt,{onChange:zt,...mn}]=_o("scrollbar",{className:_i(ce.scrollableX,ce.hideScrollbar),elementType:m_r,shouldForwardComponentProp:!0,externalForwardedProps:ze,ownerState:ue}),xn=D.useCallback(Cr=>{zt?.(Cr),rt({overflow:null,scrollbarWidth:Cr})},[zt]),[hn,Zi]=_o("scrollButtons",{className:_i(ce.scrollButtons,j.className),elementType:c_r,externalForwardedProps:ze,ownerState:ue,additionalProps:{orientation:T,slots:{StartScrollButtonIcon:M.startScrollButtonIcon||M.StartScrollButtonIcon,EndScrollButtonIcon:M.endScrollButtonIcon||M.EndScrollButtonIcon},slotProps:{startScrollButtonIcon:ye,endScrollButtonIcon:he}}}),$i=()=>{const Cr={};Cr.scrollbarSizeListener=te?k.jsx(wt,{...mn,onChange:xn}):null;const os=te&&(L==="auto"&&(Te||tt)||L===!0);return Cr.scrollButtonStart=os?k.jsx(hn,{direction:o?"right":"left",onClick:Ct,disabled:!Te,...Zi}):null,Cr.scrollButtonEnd=os?k.jsx(hn,{direction:o?"left":"right",onClick:Ye,disabled:!tt,...Zi}):null,Cr},Dr=ub(Cr=>{const{tabsMeta:Sr,tabMeta:os}=Ke();if(!(!os||!Sr)){if(os[se]Sr[de]){const Ks=Sr[ie]+(os[de]-Sr[de]);nt(Ks,{animation:Cr})}}}),ps=ub(()=>{te&&L!==!1&&He(!Me)});D.useEffect(()=>{const Cr=g5e(()=>{lt.current&&$e()});let Sr;const os=rn=>{rn.forEach(zn=>{zn.removedNodes.forEach(Oi=>{Sr?.unobserve(Oi)}),zn.addedNodes.forEach(Oi=>{Sr?.observe(Oi)})}),Cr(),ps()},Ks=Y6(lt.current);Ks.addEventListener("resize",Cr);let Ft;return typeof ResizeObserver<"u"&&(Sr=new ResizeObserver(Cr),Array.from(ct.current.children).forEach(rn=>{Sr.observe(rn)})),typeof MutationObserver<"u"&&(Ft=new MutationObserver(os),Ft.observe(ct.current,{childList:!0})),()=>{Cr.clear(),Ks.removeEventListener("resize",Cr),Ft?.disconnect(),Sr?.disconnect()}},[$e,ps]),D.useEffect(()=>{const Cr=Array.from(ct.current.children),Sr=Cr.length;if(typeof IntersectionObserver<"u"&&Sr>0&&te&&L!==!1){const os=Cr[0],Ks=Cr[Sr-1],Ft={root:lt.current,threshold:.99},rn=gs=>{Ge(!gs[0].isIntersecting)},zn=new IntersectionObserver(rn,Ft);zn.observe(os);const Oi=gs=>{Ue(!gs[0].isIntersecting)},Ki=new IntersectionObserver(Oi,Ft);return Ki.observe(Ks),()=>{zn.disconnect(),Ki.disconnect()}}},[te,L,Me,p?.length]),D.useEffect(()=>{me(!0)},[]),D.useEffect(()=>{$e()}),D.useEffect(()=>{Dr(p9n!==be)},[Dr,be]),D.useImperativeHandle(d,()=>({updateIndicator:$e,updateScrollButtons:ps}),[$e,ps]);const[nn,xt]=_o("indicator",{className:_i(ce.indicator,F.className),elementType:g_r,externalForwardedProps:ze,ownerState:ue,additionalProps:{style:be}}),Ei=k.jsx(nn,{...xt});let gr=0;const ss=D.Children.map(p,Cr=>{if(!D.isValidElement(Cr))return null;const Sr=Cr.props.value===void 0?gr:Cr.props.value;Be.set(Sr,gr);const os=Sr===q;return gr+=1,D.cloneElement(Cr,{fullWidth:Z==="fullWidth",indicator:os&&!pe&&Ei,selected:os,selectionFollowsFocus:A,onChange:x,textColor:W,value:Sr,...gr===1&&q===!1&&!Cr.props.tabIndex?{tabIndex:0}:{}})}),us=Cr=>{if(Cr.altKey||Cr.shiftKey||Cr.ctrlKey||Cr.metaKey)return;const Sr=ct.current,os=Bfe(hv(Sr));if(os?.getAttribute("role")!=="tab")return;let Ft=T==="horizontal"?"ArrowLeft":"ArrowUp",rn=T==="horizontal"?"ArrowRight":"ArrowDown";switch(T==="horizontal"&&o&&(Ft="ArrowRight",rn="ArrowLeft"),Cr.key){case Ft:Cr.preventDefault(),UBe(Sr,os,f9n);break;case rn:Cr.preventDefault(),UBe(Sr,os,h9n);break;case"Home":Cr.preventDefault(),UBe(Sr,null,h9n);break;case"End":Cr.preventDefault(),UBe(Sr,null,f9n);break}},_r=$i(),[uo,xs]=_o("root",{ref:t,className:_i(ce.root,m),elementType:h_r,externalForwardedProps:{...ze,...G,component:b},ownerState:ue}),[Fs,eo]=_o("scroller",{ref:lt,className:ce.scroller,elementType:f_r,externalForwardedProps:ze,ownerState:ue,additionalProps:{style:{overflow:at.overflow,[Q?`margin${o?"Left":"Right"}`:"marginBottom"]:ee?void 0:-at.scrollbarWidth}}}),[Ri,Ls]=_o("list",{ref:ct,className:_i(ce.list,ce.flexContainer),elementType:p_r,externalForwardedProps:ze,ownerState:ue,getSlotProps:Cr=>({...Cr,onKeyDown:Sr=>{us(Sr),Cr.onKeyDown?.(Sr)}})});return k.jsxs(uo,{...xs,children:[_r.scrollButtonStart,_r.scrollbarSizeListener,k.jsxs(Fs,{...eo,children:[k.jsx(Ri,{"aria-label":l,"aria-labelledby":c,"aria-orientation":T==="vertical"?"vertical":null,role:"tablist",...Ls,children:ss}),pe&&Ei]}),_r.scrollButtonEnd]})});function b_r(n){return No("MuiTextField",n)}Po("MuiTextField",["root"]);const v_r={standard:dUt,filled:uUt,outlined:mUt},w_r=n=>{const{classes:e}=n;return Fo({root:["root"]},b_r,e)},y_r=tn(wW,{name:"MuiTextField",slot:"Root"})({}),R3=D.forwardRef(function(e,t){const i=Wo({props:e,name:"MuiTextField"}),{autoComplete:r,autoFocus:o=!1,children:l,className:c,color:d="primary",defaultValue:h,disabled:p=!1,error:m=!1,FormHelperTextProps:b,fullWidth:w=!1,helperText:_,id:x,InputLabelProps:T,inputProps:I,InputProps:L,inputRef:A,label:M,maxRows:O,minRows:F,multiline:j=!1,name:W,onBlur:q,onChange:Z,onFocus:ee,placeholder:G,required:te=!1,rows:Q,select:ie=!1,SelectProps:se,slots:de={},slotProps:ne={},type:we,value:ue,variant:ce="outlined",...ye}=i,he={...i,autoFocus:o,color:d,disabled:p,error:m,fullWidth:w,multiline:j,required:te,select:ie,variant:ce},pe=w_r(he),me=YW(x),be=_&&me?`${me}-helper-text`:void 0,xe=M&&me?`${me}-label`:void 0,Te=v_r[ce],Ge={slots:de,slotProps:{input:L,inputLabel:T,htmlInput:I,formHelperText:b,select:se,...ne}},tt={},Ue=Ge.slotProps.inputLabel;ce==="outlined"&&(Ue&&typeof Ue.shrink<"u"&&(tt.notched=Ue.shrink),tt.label=M),ie&&((!se||!se.native)&&(tt.id=void 0),tt["aria-describedby"]=void 0);const[Me,He]=_o("root",{elementType:y_r,shouldForwardComponentProp:!0,externalForwardedProps:{...Ge,...ye},ownerState:he,className:_i(pe.root,c),ref:t,additionalProps:{disabled:p,error:m,fullWidth:w,required:te,color:d,variant:ce}}),[at,rt]=_o("input",{elementType:Te,externalForwardedProps:Ge,additionalProps:tt,ownerState:he}),[Be,lt]=_o("inputLabel",{elementType:tse,externalForwardedProps:Ge,ownerState:he}),[ct,ze]=_o("htmlInput",{elementType:"input",externalForwardedProps:Ge,ownerState:he}),[Ke,$e]=_o("formHelperText",{elementType:$br,externalForwardedProps:Ge,ownerState:he}),[nt,vt]=_o("select",{elementType:XW,externalForwardedProps:Ge,ownerState:he}),Pt=k.jsx(at,{"aria-describedby":be,autoComplete:r,autoFocus:o,defaultValue:h,fullWidth:w,multiline:j,name:W,rows:Q,maxRows:O,minRows:F,type:we,value:ue,id:me,inputRef:A,onBlur:q,onChange:Z,onFocus:ee,placeholder:G,inputProps:ze,slots:{input:de.htmlInput?ct:void 0},...rt});return k.jsxs(Me,{...He,children:[M!=null&&M!==""&&k.jsx(Be,{htmlFor:me,id:xe,...lt,children:M}),ie?k.jsx(nt,{"aria-describedby":be,id:me,labelId:xe,value:ue,input:Pt,...vt,children:l}):Pt,_&&k.jsx(Ke,{id:be,...$e,children:_})]})}),cC=fci({themeId:w3}),_W=D.forwardRef(({style:n,...e},t)=>k.jsx(b5e,{ref:t,...e,sx:{...n}})),__r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M244,88v64a12,12,0,0,1-12,12H168a12,12,0,0,1,0-24h34.9l-15.48-15.37A84,84,0,0,0,44,184a12,12,0,0,1-24,0,108,108,0,0,1,184.37-76.37L220,123.16V88a12,12,0,0,1,24,0Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232,88v64H168Z",opacity:"0.2"}),D.createElement("path",{d:"M235.06,80.61a8,8,0,0,0-8.72,1.73l-26.48,26.49A104,104,0,0,0,24,184a8,8,0,0,0,16,0,88,88,0,0,1,148.53-63.84l-26.19,26.18A8,8,0,0,0,168,160h64a8,8,0,0,0,8-8V88A8,8,0,0,0,235.06,80.61ZM224,144H187.31L224,107.31Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240,88v64a8,8,0,0,1-8,8H168a8,8,0,0,1-5.66-13.66l26.19-26.18A88,88,0,0,0,40,184a8,8,0,0,1-16,0,104,104,0,0,1,175.86-75.18l26.48-26.48A8,8,0,0,1,240,88Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M238,88v64a6,6,0,0,1-6,6H168a6,6,0,0,1,0-12h49.45l-25.8-25.63A90,90,0,0,0,38,184a6,6,0,0,1-12,0,102,102,0,0,1,174.12-72.12L226,137.58V88a6,6,0,0,1,12,0Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240,88v64a8,8,0,0,1-8,8H168a8,8,0,0,1,0-16h44.6l-22.36-22.21A88,88,0,0,0,40,184a8,8,0,0,1-16,0,104,104,0,0,1,177.54-73.54L224,132.77V88a8,8,0,0,1,16,0Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M236,88v64a4,4,0,0,1-4,4H168a4,4,0,0,1,0-8h54.3l-29.24-29A92,92,0,0,0,36,184a4,4,0,0,1-8,0,100,100,0,0,1,170.71-70.71L228,142.39V88a4,4,0,0,1,8,0Z"}))]]),C_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M244,56v48a12,12,0,0,1-12,12H184a12,12,0,1,1,0-24H201.1l-19-17.38c-.13-.12-.26-.24-.38-.37A76,76,0,1,0,127,204h1a75.53,75.53,0,0,0,52.15-20.72,12,12,0,0,1,16.49,17.45A99.45,99.45,0,0,1,128,228h-1.37A100,100,0,1,1,198.51,57.06L220,76.72V56a12,12,0,0,1,24,0Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,128a88,88,0,1,1-88-88A88,88,0,0,1,216,128Z",opacity:"0.2"}),D.createElement("path",{d:"M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1-5.66-13.66l17-17-10.55-9.65-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,1,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60l10.93,10L226.34,50.3A8,8,0,0,1,240,56Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M238,56v48a6,6,0,0,1-6,6H184a6,6,0,0,1,0-12h32.55l-30.38-27.8c-.06-.06-.12-.13-.19-.19a82,82,0,1,0-1.7,117.65,6,6,0,0,1,8.24,8.73A93.46,93.46,0,0,1,128,222h-1.28A94,94,0,1,1,194.37,61.4L226,90.35V56a6,6,0,1,1,12,0Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M236,56v48a4,4,0,0,1-4,4H184a4,4,0,0,1,0-8h37.7L187.53,68.69l-.13-.12a84,84,0,1,0-1.75,120.51,4,4,0,0,1,5.5,5.82A91.43,91.43,0,0,1,128,220h-1.26A92,92,0,1,1,193,62.84l35,32.05V56a4,4,0,1,1,8,0Z"}))]]),S_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M228,128a100,100,0,0,1-98.66,100H128a99.39,99.39,0,0,1-68.62-27.29,12,12,0,0,1,16.48-17.45,76,76,0,1,0-1.57-109c-.13.13-.25.25-.39.37L54.89,92H72a12,12,0,0,1,0,24H24a12,12,0,0,1-12-12V56a12,12,0,0,1,24,0V76.72L57.48,57.06A100,100,0,0,1,228,128Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,128a88,88,0,1,1-88-88A88,88,0,0,1,216,128Z",opacity:"0.2"}),D.createElement("path",{d:"M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L44.59,96H72a8,8,0,0,1,0,16H24a8,8,0,0,1-8-8V56a8,8,0,0,1,16,0V85.8L60.25,60A96,96,0,0,1,224,128Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L60.63,81.29l17,17A8,8,0,0,1,72,112H24a8,8,0,0,1-8-8V56A8,8,0,0,1,29.66,50.3L49.31,70,60.25,60A96,96,0,0,1,224,128Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M222,128a94,94,0,0,1-92.74,94H128a93.43,93.43,0,0,1-64.5-25.65,6,6,0,1,1,8.24-8.72A82,82,0,1,0,70,70l-.19.19L39.44,98H72a6,6,0,0,1,0,12H24a6,6,0,0,1-6-6V56a6,6,0,0,1,12,0V90.34L61.63,61.4A94,94,0,0,1,222,128Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L44.59,96H72a8,8,0,0,1,0,16H24a8,8,0,0,1-8-8V56a8,8,0,0,1,16,0V85.8L60.25,60A96,96,0,0,1,224,128Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M220,128a92,92,0,0,1-90.77,92H128a91.47,91.47,0,0,1-63.13-25.1,4,4,0,1,1,5.5-5.82A84,84,0,1,0,68.6,68.57l-.13.12L34.3,100H72a4,4,0,0,1,0,8H24a4,4,0,0,1-4-4V56a4,4,0,0,1,8,0V94.89l35-32A92,92,0,0,1,220,128Z"}))]]),x_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208.49,152.49l-72,72a12,12,0,0,1-17,0l-72-72a12,12,0,0,1,17-17L116,187V40a12,12,0,0,1,24,0V187l51.51-51.52a12,12,0,0,1,17,17Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,144l-72,72L56,144Z",opacity:"0.2"}),D.createElement("path",{d:"M207.39,140.94A8,8,0,0,0,200,136H136V40a8,8,0,0,0-16,0v96H56a8,8,0,0,0-5.66,13.66l72,72a8,8,0,0,0,11.32,0l72-72A8,8,0,0,0,207.39,140.94ZM128,204.69,75.31,152H180.69Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M205.66,149.66l-72,72a8,8,0,0,1-11.32,0l-72-72A8,8,0,0,1,56,136h64V40a8,8,0,0,1,16,0v96h64a8,8,0,0,1,5.66,13.66Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M204.24,148.24l-72,72a6,6,0,0,1-8.48,0l-72-72a6,6,0,0,1,8.48-8.48L122,201.51V40a6,6,0,0,1,12,0V201.51l61.76-61.75a6,6,0,0,1,8.48,8.48Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M205.66,149.66l-72,72a8,8,0,0,1-11.32,0l-72-72a8,8,0,0,1,11.32-11.32L120,196.69V40a8,8,0,0,1,16,0V196.69l58.34-58.35a8,8,0,0,1,11.32,11.32Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M202.83,146.83l-72,72a4,4,0,0,1-5.66,0l-72-72a4,4,0,0,1,5.66-5.66L124,206.34V40a4,4,0,0,1,8,0V206.34l65.17-65.17a4,4,0,0,1,5.66,5.66Z"}))]]),E_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M228,128a12,12,0,0,1-12,12H69l51.52,51.51a12,12,0,0,1-17,17l-72-72a12,12,0,0,1,0-17l72-72a12,12,0,0,1,17,17L69,116H216A12,12,0,0,1,228,128Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M112,56V200L40,128Z",opacity:"0.2"}),D.createElement("path",{d:"M216,120H120V56a8,8,0,0,0-13.66-5.66l-72,72a8,8,0,0,0,0,11.32l72,72A8,8,0,0,0,120,200V136h96a8,8,0,0,0,0-16ZM104,180.69,51.31,128,104,75.31Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a8,8,0,0,1-8,8H120v64a8,8,0,0,1-13.66,5.66l-72-72a8,8,0,0,1,0-11.32l72-72A8,8,0,0,1,120,56v64h96A8,8,0,0,1,224,128Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M222,128a6,6,0,0,1-6,6H54.49l61.75,61.76a6,6,0,1,1-8.48,8.48l-72-72a6,6,0,0,1,0-8.48l72-72a6,6,0,0,1,8.48,8.48L54.49,122H216A6,6,0,0,1,222,128Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M220,128a4,4,0,0,1-4,4H49.66l65.17,65.17a4,4,0,0,1-5.66,5.66l-72-72a4,4,0,0,1,0-5.66l72-72a4,4,0,0,1,5.66,5.66L49.66,124H216A4,4,0,0,1,220,128Z"}))]]),k_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224.49,136.49l-72,72a12,12,0,0,1-17-17L187,140H40a12,12,0,0,1,0-24H187L135.51,64.48a12,12,0,0,1,17-17l72,72A12,12,0,0,1,224.49,136.49Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,128l-72,72V56Z",opacity:"0.2"}),D.createElement("path",{d:"M221.66,122.34l-72-72A8,8,0,0,0,136,56v64H40a8,8,0,0,0,0,16h96v64a8,8,0,0,0,13.66,5.66l72-72A8,8,0,0,0,221.66,122.34ZM152,180.69V75.31L204.69,128Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M221.66,133.66l-72,72A8,8,0,0,1,136,200V136H40a8,8,0,0,1,0-16h96V56a8,8,0,0,1,13.66-5.66l72,72A8,8,0,0,1,221.66,133.66Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M220.24,132.24l-72,72a6,6,0,0,1-8.48-8.48L201.51,134H40a6,6,0,0,1,0-12H201.51L139.76,60.24a6,6,0,0,1,8.48-8.48l72,72A6,6,0,0,1,220.24,132.24Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M221.66,133.66l-72,72a8,8,0,0,1-11.32-11.32L196.69,136H40a8,8,0,0,1,0-16H196.69L138.34,61.66a8,8,0,0,1,11.32-11.32l72,72A8,8,0,0,1,221.66,133.66Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M218.83,130.83l-72,72a4,4,0,0,1-5.66-5.66L206.34,132H40a4,4,0,0,1,0-8H206.34L141.17,58.83a4,4,0,0,1,5.66-5.66l72,72A4,4,0,0,1,218.83,130.83Z"}))]]),T_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M228,104a12,12,0,0,1-24,0V69l-59.51,59.51a12,12,0,0,1-17-17L187,52H152a12,12,0,0,1,0-24h64a12,12,0,0,1,12,12Zm-44,24a12,12,0,0,0-12,12v64H52V84h64a12,12,0,0,0,0-24H48A20,20,0,0,0,28,80V208a20,20,0,0,0,20,20H176a20,20,0,0,0,20-20V140A12,12,0,0,0,184,128Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M184,80V208a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H176A8,8,0,0,1,184,80Z",opacity:"0.2"}),D.createElement("path",{d:"M224,104a8,8,0,0,1-16,0V59.32l-66.33,66.34a8,8,0,0,1-11.32-11.32L196.68,48H152a8,8,0,0,1,0-16h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M192,136v72a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V80A16,16,0,0,1,48,64h72a8,8,0,0,1,0,16H48V208H176V136a8,8,0,0,1,16,0Zm32-96a8,8,0,0,0-8-8H152a8,8,0,0,0-5.66,13.66L172.69,72l-42.35,42.34a8,8,0,0,0,11.32,11.32L184,83.31l26.34,26.35A8,8,0,0,0,224,104Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M222,104a6,6,0,0,1-12,0V54.49l-69.75,69.75a6,6,0,0,1-8.48-8.48L201.51,46H152a6,6,0,0,1,0-12h64a6,6,0,0,1,6,6Zm-38,26a6,6,0,0,0-6,6v72a2,2,0,0,1-2,2H48a2,2,0,0,1-2-2V80a2,2,0,0,1,2-2h72a6,6,0,0,0,0-12H48A14,14,0,0,0,34,80V208a14,14,0,0,0,14,14H176a14,14,0,0,0,14-14V136A6,6,0,0,0,184,130Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,104a8,8,0,0,1-16,0V59.32l-66.33,66.34a8,8,0,0,1-11.32-11.32L196.68,48H152a8,8,0,0,1,0-16h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M220,104a4,4,0,0,1-8,0V49.66l-73.16,73.17a4,4,0,0,1-5.66-5.66L206.34,44H152a4,4,0,0,1,0-8h64a4,4,0,0,1,4,4Zm-36,28a4,4,0,0,0-4,4v72a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V80a4,4,0,0,1,4-4h72a4,4,0,0,0,0-8H48A12,12,0,0,0,36,80V208a12,12,0,0,0,12,12H176a12,12,0,0,0,12-12V136A4,4,0,0,0,184,132Z"}))]]),L_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M236,144a68.07,68.07,0,0,1-68,68H80a12,12,0,0,1,0-24h88a44,44,0,0,0,0-88H61l27.52,27.51a12,12,0,0,1-17,17l-48-48a12,12,0,0,1,0-17l48-48a12,12,0,1,1,17,17L61,76H168A68.08,68.08,0,0,1,236,144Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M80,40v96L32,88Z",opacity:"0.2"}),D.createElement("path",{d:"M168,80H88V40a8,8,0,0,0-13.66-5.66l-48,48a8,8,0,0,0,0,11.32l48,48A8,8,0,0,0,88,136V96h80a48,48,0,0,1,0,96H80a8,8,0,0,0,0,16h88a64,64,0,0,0,0-128ZM72,116.69,43.31,88,72,59.31Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232,144a64.07,64.07,0,0,1-64,64H80a8,8,0,0,1,0-16h88a48,48,0,0,0,0-96H88v40a8,8,0,0,1-13.66,5.66l-48-48a8,8,0,0,1,0-11.32l48-48A8,8,0,0,1,88,40V80h80A64.07,64.07,0,0,1,232,144Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M230,144a62.07,62.07,0,0,1-62,62H80a6,6,0,0,1,0-12h88a50,50,0,0,0,0-100H46.49l37.75,37.76a6,6,0,1,1-8.48,8.48l-48-48a6,6,0,0,1,0-8.48l48-48a6,6,0,0,1,8.48,8.48L46.49,82H168A62.07,62.07,0,0,1,230,144Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232,144a64.07,64.07,0,0,1-64,64H80a8,8,0,0,1,0-16h88a48,48,0,0,0,0-96H51.31l34.35,34.34a8,8,0,0,1-11.32,11.32l-48-48a8,8,0,0,1,0-11.32l48-48A8,8,0,0,1,85.66,45.66L51.31,80H168A64.07,64.07,0,0,1,232,144Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M228,144a60.07,60.07,0,0,1-60,60H80a4,4,0,0,1,0-8h88a52,52,0,0,0,0-104H41.66l41.17,41.17a4,4,0,0,1-5.66,5.66l-48-48a4,4,0,0,1,0-5.66l48-48a4,4,0,0,1,5.66,5.66L41.66,84H168A60.07,60.07,0,0,1,228,144Z"}))]]),D_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216.49,56.48,177,96h19a12,12,0,0,1,0,24H148a12,12,0,0,1-12-12V60a12,12,0,0,1,24,0V79l39.51-39.52a12,12,0,0,1,17,17ZM108,136H60a12,12,0,0,0,0,24H79L39.51,199.51a12,12,0,0,0,17,17L96,177v19a12,12,0,0,0,24,0V148A12,12,0,0,0,108,136Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,48V208a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V48A16,16,0,0,1,48,32H208A16,16,0,0,1,224,48Z",opacity:"0.2"}),D.createElement("path",{d:"M213.66,53.66,163.31,104H192a8,8,0,0,1,0,16H144a8,8,0,0,1-8-8V64a8,8,0,0,1,16,0V92.69l50.34-50.35a8,8,0,0,1,11.32,11.32ZM112,136H64a8,8,0,0,0,0,16H92.69L42.34,202.34a8,8,0,0,0,11.32,11.32L104,163.31V192a8,8,0,0,0,16,0V144A8,8,0,0,0,112,136Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M120,144v48a8,8,0,0,1-13.66,5.66L88,179.31,53.66,213.66a8,8,0,0,1-11.32-11.32L76.69,168,58.34,149.66A8,8,0,0,1,64,136h48A8,8,0,0,1,120,144ZM213.66,42.34a8,8,0,0,0-11.32,0L168,76.69,149.66,58.34A8,8,0,0,0,136,64v48a8,8,0,0,0,8,8h48a8,8,0,0,0,5.66-13.66L179.31,88l34.35-34.34A8,8,0,0,0,213.66,42.34Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M212.24,52.24,158.48,106H192a6,6,0,0,1,0,12H144a6,6,0,0,1-6-6V64a6,6,0,0,1,12,0V97.52l53.76-53.76a6,6,0,0,1,8.48,8.48ZM112,138H64a6,6,0,0,0,0,12H97.52L43.76,203.76a6,6,0,1,0,8.48,8.48L106,158.48V192a6,6,0,0,0,12,0V144A6,6,0,0,0,112,138Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,53.66,163.31,104H192a8,8,0,0,1,0,16H144a8,8,0,0,1-8-8V64a8,8,0,0,1,16,0V92.69l50.34-50.35a8,8,0,0,1,11.32,11.32ZM112,136H64a8,8,0,0,0,0,16H92.69L42.34,202.34a8,8,0,0,0,11.32,11.32L104,163.31V192a8,8,0,0,0,16,0V144A8,8,0,0,0,112,136Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M210.83,50.83,153.66,108H192a4,4,0,0,1,0,8H144a4,4,0,0,1-4-4V64a4,4,0,0,1,8,0v38.34l57.17-57.17a4,4,0,1,1,5.66,5.66ZM112,140H64a4,4,0,0,0,0,8h38.34L45.17,205.17a4,4,0,0,0,5.66,5.66L108,153.66V192a4,4,0,0,0,8,0V144A4,4,0,0,0,112,140Z"}))]]),I_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M218.29,182.17a12,12,0,0,1-16.47,4.12L140,149.19V216a12,12,0,0,1-24,0V149.19l-61.82,37.1a12,12,0,1,1-12.35-20.58L104.68,128,41.83,90.29A12,12,0,1,1,54.18,69.71L116,106.81V40a12,12,0,0,1,24,0v66.81l61.82-37.1a12,12,0,1,1,12.35,20.58L151.32,128l62.85,37.71A12,12,0,0,1,218.29,182.17Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,128a72,72,0,1,1-72-72A72,72,0,0,1,200,128Z",opacity:"0.2"}),D.createElement("path",{d:"M214.86,180.12a8,8,0,0,1-11,2.74L136,142.13V216a8,8,0,0,1-16,0V142.13L52.12,182.86a8,8,0,1,1-8.23-13.72L112.45,128,43.89,86.86a8,8,0,1,1,8.23-13.72L120,113.87V40a8,8,0,0,1,16,0v73.87l67.88-40.73a8,8,0,1,1,8.23,13.72L143.55,128l68.56,41.14A8,8,0,0,1,214.86,180.12Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm59.43,129.07a8,8,0,0,1-4,14.93,7.92,7.92,0,0,1-4-1.07L136,141.86V192a8,8,0,0,1-16,0V141.86L76.57,166.93A8,8,0,0,1,65.65,164a8,8,0,0,1,2.92-10.93L112,128,68.57,102.93a8,8,0,0,1,8-13.86L120,114.14V64a8,8,0,0,1,16,0v50.14l43.43-25.07a8,8,0,0,1,8,13.86L144,128Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.14,179.09a6,6,0,0,1-8.23,2.06L134,138.6V216a6,6,0,0,1-12,0V138.6L51.09,181.15A6.07,6.07,0,0,1,48,182a6,6,0,0,1-3.1-11.15L116.34,128,44.91,85.15a6,6,0,0,1,6.18-10.3L122,117.4V40a6,6,0,0,1,12,0v77.4l70.91-42.55a6,6,0,0,1,6.18,10.3L139.66,128l71.43,42.85A6,6,0,0,1,213.14,179.09Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M214.86,180.12a8,8,0,0,1-11,2.74L136,142.13V216a8,8,0,0,1-16,0V142.13L52.12,182.86a8,8,0,1,1-8.23-13.72L112.45,128,43.89,86.86a8,8,0,1,1,8.23-13.72L120,113.87V40a8,8,0,0,1,16,0v73.87l67.88-40.73a8,8,0,1,1,8.23,13.72L143.55,128l68.56,41.14A8,8,0,0,1,214.86,180.12Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M211.43,178.06a4,4,0,0,1-5.49,1.37L132,135.06V216a4,4,0,0,1-8,0V135.06L50.06,179.43a4,4,0,0,1-4.12-6.86L120.22,128,45.94,83.43a4,4,0,0,1,4.12-6.86L124,120.94V40a4,4,0,0,1,8,0v80.94l73.94-44.37a4,4,0,1,1,4.12,6.86L135.78,128l74.28,44.57A4,4,0,0,1,211.43,178.06Z"}))]]),A_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M252,124a60.14,60.14,0,0,0-32-53.08,52,52,0,0,0-92-32.11A52,52,0,0,0,36,70.92a60,60,0,0,0,0,106.14,52,52,0,0,0,92,32.13,52,52,0,0,0,92-32.13A60.05,60.05,0,0,0,252,124ZM88,204a28,28,0,0,1-26.85-20.07c1,0,1.89.07,2.85.07h8a12,12,0,0,0,0-24H64A36,36,0,0,1,52,90.05a12,12,0,0,0,8-11.32V72a28,28,0,0,1,56,0v60.18a51.61,51.61,0,0,0-7.2-3.85,12,12,0,1,0-9.6,22A28,28,0,0,1,88,204Zm104-44h-8a12,12,0,0,0,0,24h8c1,0,1.9,0,2.85-.07a28,28,0,1,1-38-33.61,12,12,0,1,0-9.6-22,51.61,51.61,0,0,0-7.2,3.85V72a28,28,0,0,1,56,0v6.73a12,12,0,0,0,8,11.32,36,36,0,0,1-12,70Zm16-44a12,12,0,0,1-12,12,40,40,0,0,1-40-40V84a12,12,0,0,1,24,0v4a16,16,0,0,0,16,16A12,12,0,0,1,208,116ZM100,88a40,40,0,0,1-40,40,12,12,0,0,1,0-24A16,16,0,0,0,76,88V84a12,12,0,0,1,24,0Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240,124a48,48,0,0,1-32,45.27h0V176a40,40,0,0,1-80,0,40,40,0,0,1-80,0v-6.73h0a48,48,0,0,1,0-90.54V72a40,40,0,0,1,80,0,40,40,0,0,1,80,0v6.73A48,48,0,0,1,240,124Z",opacity:"0.2"}),D.createElement("path",{d:"M248,124a56.11,56.11,0,0,0-32-50.61V72a48,48,0,0,0-88-26.49A48,48,0,0,0,40,72v1.39a56,56,0,0,0,0,101.2V176a48,48,0,0,0,88,26.49A48,48,0,0,0,216,176v-1.41A56.09,56.09,0,0,0,248,124ZM88,208a32,32,0,0,1-31.81-28.56A55.87,55.87,0,0,0,64,180h8a8,8,0,0,0,0-16H64A40,40,0,0,1,50.67,86.27,8,8,0,0,0,56,78.73V72a32,32,0,0,1,64,0v68.26A47.8,47.8,0,0,0,88,128a8,8,0,0,0,0,16,32,32,0,0,1,0,64Zm104-44h-8a8,8,0,0,0,0,16h8a55.87,55.87,0,0,0,7.81-.56A32,32,0,1,1,168,144a8,8,0,0,0,0-16,47.8,47.8,0,0,0-32,12.26V72a32,32,0,0,1,64,0v6.73a8,8,0,0,0,5.33,7.54A40,40,0,0,1,192,164Zm16-52a8,8,0,0,1-8,8h-4a36,36,0,0,1-36-36V80a8,8,0,0,1,16,0v4a20,20,0,0,0,20,20h4A8,8,0,0,1,208,112ZM60,120H56a8,8,0,0,1,0-16h4A20,20,0,0,0,80,84V80a8,8,0,0,1,16,0v4A36,36,0,0,1,60,120Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M212,76V72a44,44,0,0,0-74.86-31.31,3.93,3.93,0,0,0-1.14,2.8v88.72a4,4,0,0,0,6.2,3.33A47.67,47.67,0,0,1,167.68,128a8.18,8.18,0,0,1,8.31,7.58,8,8,0,0,1-8,8.42,32,32,0,0,0-32,32v33.88a4,4,0,0,0,1.49,3.12,47.92,47.92,0,0,0,74.21-17.16,4,4,0,0,0-4.49-5.56A68.06,68.06,0,0,1,192,192h-7.73a8.18,8.18,0,0,1-8.25-7.47,8,8,0,0,1,8-8.53h8a51.6,51.6,0,0,0,24-5.88v0A52,52,0,0,0,212,76Zm-12,36h-4a36,36,0,0,1-36-36V72a8,8,0,0,1,16,0v4a20,20,0,0,0,20,20h4a8,8,0,0,1,0,16ZM88,28A44.05,44.05,0,0,0,44,72v4a52,52,0,0,0-4,94.12h0A51.6,51.6,0,0,0,64,176h7.73A8.18,8.18,0,0,1,80,183.47,8,8,0,0,1,72,192H64a67.48,67.48,0,0,1-15.21-1.73,4,4,0,0,0-4.5,5.55A47.93,47.93,0,0,0,118.51,213a4,4,0,0,0,1.49-3.12V176a32,32,0,0,0-32-32,8,8,0,0,1-8-8.42A8.18,8.18,0,0,1,88.32,128a47.67,47.67,0,0,1,25.48,7.54,4,4,0,0,0,6.2-3.33V43.49a4,4,0,0,0-1.14-2.81A43.85,43.85,0,0,0,88,28Zm8,48a36,36,0,0,1-36,36H56a8,8,0,0,1,0-16h4A20,20,0,0,0,80,76V72a8,8,0,0,1,16,0Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M246,124a54.13,54.13,0,0,0-32-49.33V72a46,46,0,0,0-86-22.67A46,46,0,0,0,42,72v2.67a54,54,0,0,0,0,98.63V176a46,46,0,0,0,86,22.67A46,46,0,0,0,214,176v-2.7A54.07,54.07,0,0,0,246,124ZM88,210a34,34,0,0,1-34-32.94A53.67,53.67,0,0,0,64,178h8a6,6,0,0,0,0-12H64A42,42,0,0,1,50,84.39a6,6,0,0,0,4-5.66V72a34,34,0,0,1,68,0v73.05A45.89,45.89,0,0,0,88,130a6,6,0,0,0,0,12,34,34,0,0,1,0,68Zm104-44h-8a6,6,0,0,0,0,12h8a53.67,53.67,0,0,0,10-.94A34,34,0,1,1,168,142a6,6,0,0,0,0-12,45.89,45.89,0,0,0-34,15.05V72a34,34,0,0,1,68,0v6.73a6,6,0,0,0,4,5.66A42,42,0,0,1,192,166Zm14-54a6,6,0,0,1-6,6h-4a34,34,0,0,1-34-34V80a6,6,0,0,1,12,0v4a22,22,0,0,0,22,22h4A6,6,0,0,1,206,112ZM60,118H56a6,6,0,0,1,0-12h4A22,22,0,0,0,82,84V80a6,6,0,0,1,12,0v4A34,34,0,0,1,60,118Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M248,124a56.11,56.11,0,0,0-32-50.61V72a48,48,0,0,0-88-26.49A48,48,0,0,0,40,72v1.39a56,56,0,0,0,0,101.2V176a48,48,0,0,0,88,26.49A48,48,0,0,0,216,176v-1.41A56.09,56.09,0,0,0,248,124ZM88,208a32,32,0,0,1-31.81-28.56A55.87,55.87,0,0,0,64,180h8a8,8,0,0,0,0-16H64A40,40,0,0,1,50.67,86.27,8,8,0,0,0,56,78.73V72a32,32,0,0,1,64,0v68.26A47.8,47.8,0,0,0,88,128a8,8,0,0,0,0,16,32,32,0,0,1,0,64Zm104-44h-8a8,8,0,0,0,0,16h8a55.87,55.87,0,0,0,7.81-.56A32,32,0,1,1,168,144a8,8,0,0,0,0-16,47.8,47.8,0,0,0-32,12.26V72a32,32,0,0,1,64,0v6.73a8,8,0,0,0,5.33,7.54A40,40,0,0,1,192,164Zm16-52a8,8,0,0,1-8,8h-4a36,36,0,0,1-36-36V80a8,8,0,0,1,16,0v4a20,20,0,0,0,20,20h4A8,8,0,0,1,208,112ZM60,120H56a8,8,0,0,1,0-16h4A20,20,0,0,0,80,84V80a8,8,0,0,1,16,0v4A36,36,0,0,1,60,120Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M244,124a52.1,52.1,0,0,0-32-48V72a44,44,0,0,0-84-18.3A44,44,0,0,0,44,72v4a52,52,0,0,0,0,96v4a44,44,0,0,0,84,18.3A44,44,0,0,0,212,176v-4A52.07,52.07,0,0,0,244,124ZM88,212a36,36,0,0,1-36-36v-1.41A52.13,52.13,0,0,0,64,176h8a4,4,0,0,0,0-8H64A44,44,0,0,1,49.33,82.5,4,4,0,0,0,52,78.73V72a36,36,0,0,1,72,0v78.75A44,44,0,0,0,88,132a4,4,0,0,0,0,8,36,36,0,0,1,0,72Zm104-44h-8a4,4,0,0,0,0,8h8a52.13,52.13,0,0,0,12-1.41V176a36,36,0,1,1-36-36,4,4,0,0,0,0-8,44,44,0,0,0-36,18.75V72a36,36,0,0,1,72,0v6.73a4,4,0,0,0,2.67,3.77A44,44,0,0,1,192,168Zm12-56a4,4,0,0,1-4,4h-4a32,32,0,0,1-32-32V80a4,4,0,0,1,8,0v4a24,24,0,0,0,24,24h4A4,4,0,0,1,204,112ZM92,84a32,32,0,0,1-32,32H56a4,4,0,0,1,0-8h4A24,24,0,0,0,84,84V80a4,4,0,0,1,8,0Z"}))]]),R_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,84a44,44,0,1,0,44,44A44.05,44.05,0,0,0,128,84Zm0,64a20,20,0,1,1,20-20A20,20,0,0,1,128,148Zm77.39,12.7A83.94,83.94,0,0,1,190.61,184a12,12,0,0,1-17.89-16,59.92,59.92,0,0,0,0-80,12,12,0,0,1,17.89-16,84.07,84.07,0,0,1,14.78,88.7ZM83.28,168a12,12,0,0,1-17.89,16,83.94,83.94,0,0,1,0-112A12,12,0,0,1,83.28,88a59.92,59.92,0,0,0,0,80ZM252,128a123.63,123.63,0,0,1-35.43,86.78A12,12,0,1,1,199.43,198a99.88,99.88,0,0,0,0-140,12,12,0,0,1,17.14-16.8A123.63,123.63,0,0,1,252,128ZM56.57,198a12,12,0,0,1-17.14,16.8,123.89,123.89,0,0,1,0-173.56A12,12,0,0,1,56.57,58a99.88,99.88,0,0,0,0,140Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M160,128a32,32,0,1,1-32-32A32,32,0,0,1,160,128Z",opacity:"0.2"}),D.createElement("path",{d:"M128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Zm73.71,7.14a80,80,0,0,1-14.08,22.2,8,8,0,0,1-11.92-10.67,63.95,63.95,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67,80.08,80.08,0,0,1,14.08,84.47ZM69,103.09a64,64,0,0,0,11.26,67.58,8,8,0,0,1-11.92,10.67,79.93,79.93,0,0,1,0-106.67A8,8,0,1,1,80.29,85.34,63.77,63.77,0,0,0,69,103.09ZM248,128a119.58,119.58,0,0,1-34.29,84,8,8,0,1,1-11.42-11.2,103.9,103.9,0,0,0,0-145.56A8,8,0,1,1,213.71,44,119.58,119.58,0,0,1,248,128ZM53.71,200.78A8,8,0,1,1,42.29,212a119.87,119.87,0,0,1,0-168,8,8,0,1,1,11.42,11.2,103.9,103.9,0,0,0,0,145.56Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M168,128a40,40,0,1,1-40-40A40,40,0,0,1,168,128Zm40,0a79.74,79.74,0,0,0-20.37-53.33,8,8,0,1,0-11.92,10.67,64,64,0,0,1,0,85.33,8,8,0,0,0,11.92,10.67A79.79,79.79,0,0,0,208,128ZM80.29,85.34A8,8,0,1,0,68.37,74.67a79.94,79.94,0,0,0,0,106.67,8,8,0,0,0,11.92-10.67,63.95,63.95,0,0,1,0-85.33Zm158.28-4A119.48,119.48,0,0,0,213.71,44a8,8,0,1,0-11.42,11.2,103.9,103.9,0,0,1,0,145.56A8,8,0,1,0,213.71,212,120.12,120.12,0,0,0,238.57,81.29ZM32.17,168.48A103.9,103.9,0,0,1,53.71,55.22,8,8,0,1,0,42.29,44a119.87,119.87,0,0,0,0,168,8,8,0,1,0,11.42-11.2A103.61,103.61,0,0,1,32.17,168.48Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,90a38,38,0,1,0,38,38A38,38,0,0,0,128,90Zm0,64a26,26,0,1,1,26-26A26,26,0,0,1,128,154Zm78-26a77.74,77.74,0,0,1-19.86,52,6,6,0,0,1-8.94-8,65.93,65.93,0,0,0,0-88,6,6,0,1,1,8.94-8A77.74,77.74,0,0,1,206,128ZM67.18,102.31A65.93,65.93,0,0,0,78.8,172a6,6,0,0,1-.47,8.47,6,6,0,0,1-8.47-.47,77.93,77.93,0,0,1,0-104,6,6,0,1,1,8.94,8A66.21,66.21,0,0,0,67.18,102.31ZM246,128a117.71,117.71,0,0,1-33.71,82.58,6,6,0,0,1-8.58-8.4,105.88,105.88,0,0,0,0-148.36,6,6,0,0,1,8.58-8.4A117.71,117.71,0,0,1,246,128ZM52.29,202.18a6,6,0,0,1-8.58,8.4,117.92,117.92,0,0,1,0-165.16,6,6,0,1,1,8.58,8.4,105.88,105.88,0,0,0,0,148.36Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Zm73.71,7.14a80,80,0,0,1-14.08,22.2,8,8,0,0,1-11.92-10.67,63.95,63.95,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67,80.08,80.08,0,0,1,14.08,84.47ZM69,103.09a64,64,0,0,0,11.26,67.58,8,8,0,0,1-11.92,10.67,79.93,79.93,0,0,1,0-106.67A8,8,0,1,1,80.29,85.34,63.77,63.77,0,0,0,69,103.09ZM248,128a119.58,119.58,0,0,1-34.29,84,8,8,0,1,1-11.42-11.2,103.9,103.9,0,0,0,0-145.56A8,8,0,1,1,213.71,44,119.58,119.58,0,0,1,248,128ZM53.71,200.78A8,8,0,1,1,42.29,212a119.87,119.87,0,0,1,0-168,8,8,0,1,1,11.42,11.2,103.9,103.9,0,0,0,0,145.56Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,92a36,36,0,1,0,36,36A36,36,0,0,0,128,92Zm0,64a28,28,0,1,1,28-28A28,28,0,0,1,128,156Zm76-28a75.74,75.74,0,0,1-19.35,50.67,4,4,0,0,1-6-5.34,67.92,67.92,0,0,0,0-90.66,4,4,0,0,1,6-5.34A75.74,75.74,0,0,1,204,128ZM65.34,101.53a67.92,67.92,0,0,0,12,71.8,4,4,0,0,1-6,5.34,75.93,75.93,0,0,1,0-101.34,4,4,0,1,1,6,5.34A68,68,0,0,0,65.34,101.53ZM244,128a115.68,115.68,0,0,1-33.14,81.18,4,4,0,0,1-5.72-5.6,107.89,107.89,0,0,0,0-151.16,4,4,0,0,1,5.72-5.6A115.68,115.68,0,0,1,244,128ZM50.86,203.58a4,4,0,0,1-5.72,5.6,115.91,115.91,0,0,1,0-162.36,4,4,0,1,1,5.72,5.6,107.89,107.89,0,0,0,0,151.16Z"}))]]),M_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M180,72H36A20,20,0,0,0,16,92V204a20,20,0,0,0,20,20H180a20,20,0,0,0,20-20V92A20,20,0,0,0,180,72Zm-4,128H40V96H176ZM240,52V176a12,12,0,0,1-24,0V56H64a12,12,0,0,1,0-24H220A20,20,0,0,1,240,52Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M192,88V200a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V88a8,8,0,0,1,8-8H184A8,8,0,0,1,192,88Z",opacity:"0.2"}),D.createElement("path",{d:"M184,72H40A16,16,0,0,0,24,88V200a16,16,0,0,0,16,16H184a16,16,0,0,0,16-16V88A16,16,0,0,0,184,72Zm0,128H40V88H184V200ZM232,56V176a8,8,0,0,1-16,0V56H64a8,8,0,0,1,0-16H216A16,16,0,0,1,232,56Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,88V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V88A16,16,0,0,1,40,72H184A16,16,0,0,1,200,88Zm16-48H64a8,8,0,0,0,0,16H216V176a8,8,0,0,0,16,0V56A16,16,0,0,0,216,40Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M184,74H40A14,14,0,0,0,26,88V200a14,14,0,0,0,14,14H184a14,14,0,0,0,14-14V88A14,14,0,0,0,184,74Zm2,126a2,2,0,0,1-2,2H40a2,2,0,0,1-2-2V88a2,2,0,0,1,2-2H184a2,2,0,0,1,2,2ZM230,56V176a6,6,0,0,1-12,0V56a2,2,0,0,0-2-2H64a6,6,0,0,1,0-12H216A14,14,0,0,1,230,56Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M184,72H40A16,16,0,0,0,24,88V200a16,16,0,0,0,16,16H184a16,16,0,0,0,16-16V88A16,16,0,0,0,184,72Zm0,128H40V88H184V200ZM232,56V176a8,8,0,0,1-16,0V56H64a8,8,0,0,1,0-16H216A16,16,0,0,1,232,56Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M184,76H40A12,12,0,0,0,28,88V200a12,12,0,0,0,12,12H184a12,12,0,0,0,12-12V88A12,12,0,0,0,184,76Zm4,124a4,4,0,0,1-4,4H40a4,4,0,0,1-4-4V88a4,4,0,0,1,4-4H184a4,4,0,0,1,4,4ZM228,56V176a4,4,0,0,1-8,0V56a4,4,0,0,0-4-4H64a4,4,0,0,1,0-8H216A12,12,0,0,1,228,56Z"}))]]),O_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M144.49,136.49l-80,80a12,12,0,0,1-17-17L119,128,47.51,56.49a12,12,0,0,1,17-17l80,80A12,12,0,0,1,144.49,136.49Zm80-17-80-80a12,12,0,1,0-17,17L199,128l-71.52,71.51a12,12,0,0,0,17,17l80-80A12,12,0,0,0,224.49,119.51Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M136,128,56,208V48Z",opacity:"0.2"}),D.createElement("path",{d:"M141.66,122.34l-80-80A8,8,0,0,0,48,48V208a8,8,0,0,0,13.66,5.66l80-80A8,8,0,0,0,141.66,122.34ZM64,188.69V67.31L124.69,128Zm157.66-55-80,80a8,8,0,0,1-11.32-11.32L204.69,128,130.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,221.66,133.66Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M221.66,133.66l-80,80A8,8,0,0,1,128,208V147.31L61.66,213.66A8,8,0,0,1,48,208V48a8,8,0,0,1,13.66-5.66L128,108.69V48a8,8,0,0,1,13.66-5.66l80,80A8,8,0,0,1,221.66,133.66Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M140.24,132.24l-80,80a6,6,0,0,1-8.48-8.48L127.51,128,51.76,52.24a6,6,0,0,1,8.48-8.48l80,80A6,6,0,0,1,140.24,132.24Zm80-8.48-80-80a6,6,0,0,0-8.48,8.48L207.51,128l-75.75,75.76a6,6,0,1,0,8.48,8.48l80-80A6,6,0,0,0,220.24,123.76Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M141.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L124.69,128,50.34,53.66A8,8,0,0,1,61.66,42.34l80,80A8,8,0,0,1,141.66,133.66Zm80-11.32-80-80a8,8,0,0,0-11.32,11.32L204.69,128l-74.35,74.34a8,8,0,0,0,11.32,11.32l80-80A8,8,0,0,0,221.66,122.34Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M138.83,130.83l-80,80a4,4,0,0,1-5.66-5.66L130.34,128,53.17,50.83a4,4,0,0,1,5.66-5.66l80,80A4,4,0,0,1,138.83,130.83Zm80-5.66-80-80a4,4,0,0,0-5.66,5.66L210.34,128l-77.17,77.17a4,4,0,0,0,5.66,5.66l80-80A4,4,0,0,0,218.83,125.17Z"}))]]),N_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216.49,104.49l-80,80a12,12,0,0,1-17,0l-80-80a12,12,0,0,1,17-17L128,159l71.51-71.52a12,12,0,0,1,17,17Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,96l-80,80L48,96Z",opacity:"0.2"}),D.createElement("path",{d:"M215.39,92.94A8,8,0,0,0,208,88H48a8,8,0,0,0-5.66,13.66l80,80a8,8,0,0,0,11.32,0l80-80A8,8,0,0,0,215.39,92.94ZM128,164.69,67.31,104H188.69Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M212.24,100.24l-80,80a6,6,0,0,1-8.48,0l-80-80a6,6,0,0,1,8.48-8.48L128,167.51l75.76-75.75a6,6,0,0,1,8.48,8.48Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M210.83,98.83l-80,80a4,4,0,0,1-5.66,0l-80-80a4,4,0,0,1,5.66-5.66L128,170.34l77.17-77.17a4,4,0,1,1,5.66,5.66Z"}))]]),P_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M184.49,136.49l-80,80a12,12,0,0,1-17-17L159,128,87.51,56.49a12,12,0,1,1,17-17l80,80A12,12,0,0,1,184.49,136.49Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M176,128,96,208V48Z",opacity:"0.2"}),D.createElement("path",{d:"M181.66,122.34l-80-80A8,8,0,0,0,88,48V208a8,8,0,0,0,13.66,5.66l80-80A8,8,0,0,0,181.66,122.34ZM104,188.69V67.31L164.69,128Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M181.66,133.66l-80,80A8,8,0,0,1,88,208V48a8,8,0,0,1,13.66-5.66l80,80A8,8,0,0,1,181.66,133.66Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M180.24,132.24l-80,80a6,6,0,0,1-8.48-8.48L167.51,128,91.76,52.24a6,6,0,0,1,8.48-8.48l80,80A6,6,0,0,1,180.24,132.24Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M178.83,130.83l-80,80a4,4,0,0,1-5.66-5.66L170.34,128,93.17,50.83a4,4,0,0,1,5.66-5.66l80,80A4,4,0,0,1,178.83,130.83Z"}))]]),F_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216.49,168.49a12,12,0,0,1-17,0L128,97,56.49,168.49a12,12,0,0,1-17-17l80-80a12,12,0,0,1,17,0l80,80A12,12,0,0,1,216.49,168.49Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,160H48l80-80Z",opacity:"0.2"}),D.createElement("path",{d:"M213.66,154.34l-80-80a8,8,0,0,0-11.32,0l-80,80A8,8,0,0,0,48,168H208a8,8,0,0,0,5.66-13.66ZM67.31,152,128,91.31,188.69,152Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M215.39,163.06A8,8,0,0,1,208,168H48a8,8,0,0,1-5.66-13.66l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,215.39,163.06Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M212.24,164.24a6,6,0,0,1-8.48,0L128,88.49,52.24,164.24a6,6,0,0,1-8.48-8.48l80-80a6,6,0,0,1,8.48,0l80,80A6,6,0,0,1,212.24,164.24Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,165.66a8,8,0,0,1-11.32,0L128,91.31,53.66,165.66a8,8,0,0,1-11.32-11.32l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,213.66,165.66Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M210.83,162.83a4,4,0,0,1-5.66,0L128,85.66,50.83,162.83a4,4,0,0,1-5.66-5.66l80-80a4,4,0,0,1,5.66,0l80,80A4,4,0,0,1,210.83,162.83Z"}))]]),j_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,20A108,108,0,0,0,31.85,177.23L21,209.66A20,20,0,0,0,46.34,235l32.43-10.81A108,108,0,1,0,128,20Zm0,192a84,84,0,0,1-42.06-11.27,12,12,0,0,0-6-1.62,12.1,12.1,0,0,0-3.8.62l-29.79,9.93,9.93-29.79a12,12,0,0,0-1-9.81A84,84,0,1,1,128,212Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128A96,96,0,0,1,79.93,211.11h0L42.54,223.58a8,8,0,0,1-10.12-10.12l12.47-37.39h0A96,96,0,1,1,224,128Z",opacity:"0.2"}),D.createElement("path",{d:"M128,24A104,104,0,0,0,36.18,176.88L24.83,210.93a16,16,0,0,0,20.24,20.24l34.05-11.35A104,104,0,1,0,128,24Zm0,192a87.87,87.87,0,0,1-44.06-11.81,8,8,0,0,0-6.54-.67L40,216,52.47,178.6a8,8,0,0,0-.66-6.54A88,88,0,1,1,128,216Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232,128A104,104,0,0,1,79.12,219.82L45.07,231.17a16,16,0,0,1-20.24-20.24l11.35-34.05A104,104,0,1,1,232,128Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,26A102,102,0,0,0,38.35,176.69L26.73,211.56a14,14,0,0,0,17.71,17.71l34.87-11.62A102,102,0,1,0,128,26Zm0,192a90,90,0,0,1-45.06-12.08,6.09,6.09,0,0,0-3-.81,6.2,6.2,0,0,0-1.9.31L40.65,217.88a2,2,0,0,1-2.53-2.53L50.58,178a6,6,0,0,0-.5-4.91A90,90,0,1,1,128,218Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24A104,104,0,0,0,36.18,176.88L24.83,210.93a16,16,0,0,0,20.24,20.24l34.05-11.35A104,104,0,1,0,128,24Zm0,192a87.87,87.87,0,0,1-44.06-11.81,8,8,0,0,0-6.54-.67L40,216,52.47,178.6a8,8,0,0,0-.66-6.54A88,88,0,1,1,128,216Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,28A100,100,0,0,0,40.53,176.5l-11.9,35.69a12,12,0,0,0,15.18,15.18l35.69-11.9A100,100,0,1,0,128,28Zm0,192a92,92,0,0,1-46.07-12.35,4.05,4.05,0,0,0-2-.54,3.93,3.93,0,0,0-1.27.21L41.28,219.78a4,4,0,0,1-5.06-5.06l12.46-37.38a4,4,0,0,0-.33-3.27A92,92,0,1,1,128,220Z"}))]]),H_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232,56V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56Z",opacity:"0.2"}),D.createElement("path",{d:"M205.66,85.66l-96,96a8,8,0,0,1-11.32,0l-40-40a8,8,0,0,1,11.32-11.32L104,164.69l90.34-90.35a8,8,0,0,1,11.32,11.32Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM205.66,85.66l-96,96a8,8,0,0,1-11.32,0l-40-40a8,8,0,0,1,11.32-11.32L104,164.69l90.34-90.35a8,8,0,0,1,11.32,11.32Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M228.24,76.24l-128,128a6,6,0,0,1-8.48,0l-56-56a6,6,0,0,1,8.48-8.48L96,191.51,219.76,67.76a6,6,0,0,1,8.48,8.48Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M226.83,74.83l-128,128a4,4,0,0,1-5.66,0l-56-56a4,4,0,0,1,5.66-5.66L96,194.34,221.17,69.17a4,4,0,1,1,5.66,5.66Z"}))]]),B_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M176.49,95.51a12,12,0,0,1,0,17l-56,56a12,12,0,0,1-17,0l-24-24a12,12,0,1,1,17-17L112,143l47.51-47.52A12,12,0,0,1,176.49,95.51ZM236,128A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-24,0a84,84,0,1,0-84,84A84.09,84.09,0,0,0,212,128Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z",opacity:"0.2"}),D.createElement("path",{d:"M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,85.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M172.24,99.76a6,6,0,0,1,0,8.48l-56,56a6,6,0,0,1-8.48,0l-24-24a6,6,0,0,1,8.48-8.48L112,151.51l51.76-51.75A6,6,0,0,1,172.24,99.76ZM230,128A102,102,0,1,1,128,26,102.12,102.12,0,0,1,230,128Zm-12,0a90,90,0,1,0-90,90A90.1,90.1,0,0,0,218,128Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M170.83,101.17a4,4,0,0,1,0,5.66l-56,56a4,4,0,0,1-5.66,0l-24-24a4,4,0,0,1,5.66-5.66L112,154.34l53.17-53.17A4,4,0,0,1,170.83,101.17ZM228,128A100,100,0,1,1,128,28,100.11,100.11,0,0,1,228,128Zm-8,0a92,92,0,1,0-92,92A92.1,92.1,0,0,0,220,128Z"}))]]),W_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm68-84a12,12,0,0,1-12,12H128a12,12,0,0,1-12-12V72a12,12,0,0,1,24,0v44h44A12,12,0,0,1,196,128Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z",opacity:"0.2"}),D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm64-88a8,8,0,0,1-8,8H128a8,8,0,0,1-8-8V72a8,8,0,0,1,16,0v48h48A8,8,0,0,1,192,128Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm56,112H128a8,8,0,0,1-8-8V72a8,8,0,0,1,16,0v48h48a8,8,0,0,1,0,16Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,26A102,102,0,1,0,230,128,102.12,102.12,0,0,0,128,26Zm0,192a90,90,0,1,1,90-90A90.1,90.1,0,0,1,128,218Zm62-90a6,6,0,0,1-6,6H128a6,6,0,0,1-6-6V72a6,6,0,0,1,12,0v50h50A6,6,0,0,1,190,128Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm64-88a8,8,0,0,1-8,8H128a8,8,0,0,1-8-8V72a8,8,0,0,1,16,0v48h48A8,8,0,0,1,192,128Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,28A100,100,0,1,0,228,128,100.11,100.11,0,0,0,128,28Zm0,192a92,92,0,1,1,92-92A92.1,92.1,0,0,1,128,220Zm60-92a4,4,0,0,1-4,4H128a4,4,0,0,1-4-4V72a4,4,0,0,1,8,0v52h52A4,4,0,0,1,188,128Z"}))]]),V_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M140,80v41.21l34.17,20.5a12,12,0,1,1-12.34,20.58l-40-24A12,12,0,0,1,116,128V80a12,12,0,0,1,24,0Zm84-28a12,12,0,0,0-12,12v7.37c-4.21-4.67-8.58-9.31-13.29-14.08a100,100,0,1,0-2.07,143.44,12,12,0,0,0-16.48-17.46,76,76,0,1,1,1.53-109.06C187.61,80.2,193,86,198.23,92H184a12,12,0,0,0,0,24h40a12,12,0,0,0,12-12V64A12,12,0,0,0,224,52Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,128a88,88,0,1,1-88-88A88,88,0,0,1,216,128Z",opacity:"0.2"}),D.createElement("path",{d:"M136,80v43.47l36.12,21.67a8,8,0,0,1-8.24,13.72l-40-24A8,8,0,0,1,120,128V80a8,8,0,0,1,16,0Zm88-24a8,8,0,0,0-8,8V82c-6.35-7.36-12.83-14.45-20.12-21.83a96,96,0,1,0-2,137.7,8,8,0,0,0-11-11.64A80,80,0,1,1,184.54,71.4C192.68,79.64,199.81,87.58,207,96H184a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V64A8,8,0,0,0,224,56Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M136,80v43.47l36.12,21.67a8,8,0,0,1-8.24,13.72l-40-24A8,8,0,0,1,120,128V80a8,8,0,0,1,16,0Zm91.06-23.39a8,8,0,0,0-8.72,1.73L206,70.71c-3.23-3.51-6.56-7-10.1-10.59a96,96,0,1,0-2,137.7,8,8,0,0,0-11-11.64A80,80,0,1,1,184.54,71.4c3.54,3.58,6.87,7.1,10.11,10.63L178.34,98.34A8,8,0,0,0,184,112h40a8,8,0,0,0,8-8V64A8,8,0,0,0,227.06,56.61Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M134,80v44.6l37.09,22.25a6,6,0,0,1-6.18,10.3l-40-24A6,6,0,0,1,122,128V80a6,6,0,0,1,12,0Zm90-22a6,6,0,0,0-6,6V87.36c-7.48-8.83-14.94-17.13-23.53-25.83a94,94,0,1,0-1.95,134.83,6,6,0,0,0-8.24-8.72A82,82,0,1,1,186,70c9.24,9.36,17.18,18.3,25.31,28H184a6,6,0,0,0,0,12h40a6,6,0,0,0,6-6V64A6,6,0,0,0,224,58Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M136,80v43.47l36.12,21.67a8,8,0,0,1-8.24,13.72l-40-24A8,8,0,0,1,120,128V80a8,8,0,0,1,16,0Zm88-24a8,8,0,0,0-8,8V82c-6.35-7.36-12.83-14.45-20.12-21.83a96,96,0,1,0-2,137.7,8,8,0,0,0-11-11.64A80,80,0,1,1,184.54,71.4C192.68,79.64,199.81,87.58,207,96H184a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V64A8,8,0,0,0,224,56Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M132,80v45.74l38.06,22.83a4,4,0,0,1-4.12,6.86l-40-24A4,4,0,0,1,124,128V80a4,4,0,0,1,8,0Zm92-20a4,4,0,0,0-4,4V92.85C211.33,82.46,203,73,193.05,63a92,92,0,1,0-1.9,132,4,4,0,0,0-5.5-5.82,84,84,0,1,1,1.73-120.5C197.7,79,206.39,89,215.53,100H184a4,4,0,0,0,0,8h40a4,4,0,0,0,4-4V64A4,4,0,0,0,224,60Z"}))]]),$_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M192.49,167.51a12,12,0,0,1,0,17l-32,32a12,12,0,0,1-17,0l-32-32a12,12,0,1,1,17-17L140,179V128a12,12,0,0,1,24,0v51l11.51-11.52A12,12,0,0,1,192.49,167.51ZM160,36A92.08,92.08,0,0,0,79,84.37,68,68,0,1,0,72,220H84a12,12,0,0,0,0-24H72a44,44,0,0,1-1.81-87.95A91.7,91.7,0,0,0,68,128a12,12,0,0,0,24,0,68,68,0,0,1,136,0,67.27,67.27,0,0,1-7.25,30.59,12,12,0,1,0,21.42,10.82A91.08,91.08,0,0,0,252,128,92.1,92.1,0,0,0,160,36Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240,128a80,80,0,0,1-80,80H72A56,56,0,1,1,85.92,97.74l0,.1A80,80,0,0,1,240,128Z",opacity:"0.2"}),D.createElement("path",{d:"M248,128a87.34,87.34,0,0,1-17.6,52.81,8,8,0,1,1-12.8-9.62A71.34,71.34,0,0,0,232,128a72,72,0,0,0-144,0,8,8,0,0,1-16,0,88,88,0,0,1,3.29-23.88C74.2,104,73.1,104,72,104a48,48,0,0,0,0,96H96a8,8,0,0,1,0,16H72A64,64,0,1,1,81.29,88.68,88,88,0,0,1,248,128Zm-69.66,42.34L160,188.69V128a8,8,0,0,0-16,0v60.69l-18.34-18.35a8,8,0,0,0-11.32,11.32l32,32a8,8,0,0,0,11.32,0l32-32a8,8,0,0,0-11.32-11.32Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M247.93,124.52C246.11,77.54,207.07,40,160.06,40A88.1,88.1,0,0,0,81.29,88.67h0A87.48,87.48,0,0,0,72,127.73,8.18,8.18,0,0,1,64.57,136,8,8,0,0,1,56,128a103.66,103.66,0,0,1,5.34-32.92,4,4,0,0,0-4.75-5.18A64.09,64.09,0,0,0,8,152c0,35.19,29.75,64,65,64H160A88.09,88.09,0,0,0,247.93,124.52Zm-50.27,25.14-32,32a8,8,0,0,1-11.32,0l-32-32a8,8,0,0,1,11.32-11.32L152,156.69V96a8,8,0,0,1,16,0v60.69l18.34-18.35a8,8,0,0,1,11.32,11.32Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M246,128a85.27,85.27,0,0,1-17.2,51.6,6,6,0,1,1-9.6-7.2A74,74,0,1,0,86,128a6,6,0,0,1-12,0,85.54,85.54,0,0,1,3.91-25.64A50.68,50.68,0,0,0,72,102a50,50,0,0,0,0,100H96a6,6,0,0,1,0,12H72A62,62,0,1,1,82.43,90.88,86,86,0,0,1,246,128Zm-66.24,43.76L158,193.51V128a6,6,0,0,0-12,0v65.51l-21.76-21.75a6,6,0,0,0-8.48,8.48l32,32a6,6,0,0,0,8.48,0l32-32a6,6,0,0,0-8.48-8.48Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M248,128a87.34,87.34,0,0,1-17.6,52.81,8,8,0,1,1-12.8-9.62A71.34,71.34,0,0,0,232,128a72,72,0,0,0-144,0,8,8,0,0,1-16,0,88,88,0,0,1,3.29-23.88C74.2,104,73.1,104,72,104a48,48,0,0,0,0,96H96a8,8,0,0,1,0,16H72A64,64,0,1,1,81.29,88.68,88,88,0,0,1,248,128Zm-69.66,42.34L160,188.69V128a8,8,0,0,0-16,0v60.69l-18.34-18.35a8,8,0,0,0-11.32,11.32l32,32a8,8,0,0,0,11.32,0l32-32a8,8,0,0,0-11.32-11.32Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M244,128a83.28,83.28,0,0,1-16.8,50.4,4,4,0,1,1-6.4-4.8A76,76,0,1,0,84,128a4,4,0,0,1-8,0,83.45,83.45,0,0,1,4.57-27.27A52,52,0,1,0,72,204H96a4,4,0,0,1,0,8H72A60,60,0,1,1,83.61,93.13,84,84,0,0,1,244,128Zm-62.83,45.17L156,198.34V128a4,4,0,0,0-8,0v70.34l-25.17-25.17a4,4,0,0,0-5.66,5.66l32,32a4,4,0,0,0,5.66,0l32-32a4,4,0,0,0-5.66-5.66Z"}))]]),z_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M71.68,97.22,34.74,128l36.94,30.78a12,12,0,1,1-15.36,18.44l-48-40a12,12,0,0,1,0-18.44l48-40A12,12,0,0,1,71.68,97.22Zm176,21.56-48-40a12,12,0,1,0-15.36,18.44L221.26,128l-36.94,30.78a12,12,0,1,0,15.36,18.44l48-40a12,12,0,0,0,0-18.44ZM164.1,28.72a12,12,0,0,0-15.38,7.18l-64,176a12,12,0,0,0,7.18,15.37A11.79,11.79,0,0,0,96,228a12,12,0,0,0,11.28-7.9l64-176A12,12,0,0,0,164.1,28.72Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240,128l-48,40H64L16,128,64,88H192Z",opacity:"0.2"}),D.createElement("path",{d:"M69.12,94.15,28.5,128l40.62,33.85a8,8,0,1,1-10.24,12.29l-48-40a8,8,0,0,1,0-12.29l48-40a8,8,0,0,1,10.24,12.3Zm176,27.7-48-40a8,8,0,1,0-10.24,12.3L227.5,128l-40.62,33.85a8,8,0,1,0,10.24,12.29l48-40a8,8,0,0,0,0-12.29ZM162.73,32.48a8,8,0,0,0-10.25,4.79l-64,176a8,8,0,0,0,4.79,10.26A8.14,8.14,0,0,0,96,224a8,8,0,0,0,7.52-5.27l64-176A8,8,0,0,0,162.73,32.48Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM92.8,145.6a8,8,0,1,1-9.6,12.8l-32-24a8,8,0,0,1,0-12.8l32-24a8,8,0,0,1,9.6,12.8L69.33,128Zm58.89-71.4-32,112a8,8,0,1,1-15.38-4.4l32-112a8,8,0,0,1,15.38,4.4Zm53.11,60.2-32,24a8,8,0,0,1-9.6-12.8L186.67,128,163.2,110.4a8,8,0,1,1,9.6-12.8l32,24a8,8,0,0,1,0,12.8Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M67.84,92.61,25.37,128l42.47,35.39a6,6,0,1,1-7.68,9.22l-48-40a6,6,0,0,1,0-9.22l48-40a6,6,0,0,1,7.68,9.22Zm176,30.78-48-40a6,6,0,1,0-7.68,9.22L230.63,128l-42.47,35.39a6,6,0,1,0,7.68,9.22l48-40a6,6,0,0,0,0-9.22Zm-81.79-89A6,6,0,0,0,154.36,38l-64,176A6,6,0,0,0,94,221.64a6.15,6.15,0,0,0,2,.36,6,6,0,0,0,5.64-3.95l64-176A6,6,0,0,0,162.05,34.36Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M69.12,94.15,28.5,128l40.62,33.85a8,8,0,1,1-10.24,12.29l-48-40a8,8,0,0,1,0-12.29l48-40a8,8,0,0,1,10.24,12.3Zm176,27.7-48-40a8,8,0,1,0-10.24,12.3L227.5,128l-40.62,33.85a8,8,0,1,0,10.24,12.29l48-40a8,8,0,0,0,0-12.29ZM162.73,32.48a8,8,0,0,0-10.25,4.79l-64,176a8,8,0,0,0,4.79,10.26A8.14,8.14,0,0,0,96,224a8,8,0,0,0,7.52-5.27l64-176A8,8,0,0,0,162.73,32.48Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M66.56,91.07,22.25,128l44.31,36.93A4,4,0,0,1,64,172a3.94,3.94,0,0,1-2.56-.93l-48-40a4,4,0,0,1,0-6.14l48-40a4,4,0,0,1,5.12,6.14Zm176,33.86-48-40a4,4,0,1,0-5.12,6.14L233.75,128l-44.31,36.93a4,4,0,1,0,5.12,6.14l48-40a4,4,0,0,0,0-6.14ZM161.37,36.24a4,4,0,0,0-5.13,2.39l-64,176a4,4,0,0,0,2.39,5.13A4.12,4.12,0,0,0,96,220a4,4,0,0,0,3.76-2.63l64-176A4,4,0,0,0,161.37,36.24Z"}))]]),U_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M100,28H64A20,20,0,0,0,44,48V208a20,20,0,0,0,20,20h36a20,20,0,0,0,20-20V48A20,20,0,0,0,100,28ZM96,204H68V52H96ZM192,28H156a20,20,0,0,0-20,20V208a20,20,0,0,0,20,20h36a20,20,0,0,0,20-20V48A20,20,0,0,0,192,28Zm-4,176H160V52h28Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M112,48V208a8,8,0,0,1-8,8H64a8,8,0,0,1-8-8V48a8,8,0,0,1,8-8h40A8,8,0,0,1,112,48Zm80-8H152a8,8,0,0,0-8,8V208a8,8,0,0,0,8,8h40a8,8,0,0,0,8-8V48A8,8,0,0,0,192,40Z",opacity:"0.2"}),D.createElement("path",{d:"M104,32H64A16,16,0,0,0,48,48V208a16,16,0,0,0,16,16h40a16,16,0,0,0,16-16V48A16,16,0,0,0,104,32Zm0,176H64V48h40ZM192,32H152a16,16,0,0,0-16,16V208a16,16,0,0,0,16,16h40a16,16,0,0,0,16-16V48A16,16,0,0,0,192,32Zm0,176H152V48h40Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M120,48V208a16,16,0,0,1-16,16H64a16,16,0,0,1-16-16V48A16,16,0,0,1,64,32h40A16,16,0,0,1,120,48Zm72-16H152a16,16,0,0,0-16,16V208a16,16,0,0,0,16,16h40a16,16,0,0,0,16-16V48A16,16,0,0,0,192,32Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M104,34H64A14,14,0,0,0,50,48V208a14,14,0,0,0,14,14h40a14,14,0,0,0,14-14V48A14,14,0,0,0,104,34Zm2,174a2,2,0,0,1-2,2H64a2,2,0,0,1-2-2V48a2,2,0,0,1,2-2h40a2,2,0,0,1,2,2ZM192,34H152a14,14,0,0,0-14,14V208a14,14,0,0,0,14,14h40a14,14,0,0,0,14-14V48A14,14,0,0,0,192,34Zm2,174a2,2,0,0,1-2,2H152a2,2,0,0,1-2-2V48a2,2,0,0,1,2-2h40a2,2,0,0,1,2,2Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M104,32H64A16,16,0,0,0,48,48V208a16,16,0,0,0,16,16h40a16,16,0,0,0,16-16V48A16,16,0,0,0,104,32Zm0,176H64V48h40ZM192,32H152a16,16,0,0,0-16,16V208a16,16,0,0,0,16,16h40a16,16,0,0,0,16-16V48A16,16,0,0,0,192,32Zm0,176H152V48h40Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M104,36H64A12,12,0,0,0,52,48V208a12,12,0,0,0,12,12h40a12,12,0,0,0,12-12V48A12,12,0,0,0,104,36Zm4,172a4,4,0,0,1-4,4H64a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4h40a4,4,0,0,1,4,4ZM192,36H152a12,12,0,0,0-12,12V208a12,12,0,0,0,12,12h40a12,12,0,0,0,12-12V48A12,12,0,0,0,192,36Zm4,172a4,4,0,0,1-4,4H152a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4h40a4,4,0,0,1,4,4Z"}))]]),q_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,28H88A12,12,0,0,0,76,40V76H40A12,12,0,0,0,28,88V216a12,12,0,0,0,12,12H168a12,12,0,0,0,12-12V180h36a12,12,0,0,0,12-12V40A12,12,0,0,0,216,28ZM156,204H52V100H156Zm48-48H180V88a12,12,0,0,0-12-12H100V52H204Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,40V168H168V88H88V40Z",opacity:"0.2"}),D.createElement("path",{d:"M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32Zm-8,128H176V88a8,8,0,0,0-8-8H96V48H208Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,34H88a6,6,0,0,0-6,6V82H40a6,6,0,0,0-6,6V216a6,6,0,0,0,6,6H168a6,6,0,0,0,6-6V174h42a6,6,0,0,0,6-6V40A6,6,0,0,0,216,34ZM162,210H46V94H162Zm48-48H174V88a6,6,0,0,0-6-6H94V46H210Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,36H88a4,4,0,0,0-4,4V84H40a4,4,0,0,0-4,4V216a4,4,0,0,0,4,4H168a4,4,0,0,0,4-4V172h44a4,4,0,0,0,4-4V40A4,4,0,0,0,216,36ZM164,212H44V92H164Zm48-48H172V88a4,4,0,0,0-4-4H92V44H212Z"}))]]),G_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M180,64H40A12,12,0,0,0,28,76V216a12,12,0,0,0,12,12H180a12,12,0,0,0,12-12V76A12,12,0,0,0,180,64ZM168,204H52V88H168ZM228,40V180a12,12,0,0,1-24,0V52H76a12,12,0,0,1,0-24H216A12,12,0,0,1,228,40Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M184,72V216H40V72Z",opacity:"0.2"}),D.createElement("path",{d:"M184,64H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H184a8,8,0,0,0,8-8V72A8,8,0,0,0,184,64Zm-8,144H48V80H176ZM224,40V184a8,8,0,0,1-16,0V48H72a8,8,0,0,1,0-16H216A8,8,0,0,1,224,40Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M192,72V216a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V72a8,8,0,0,1,8-8H184A8,8,0,0,1,192,72Zm24-40H72a8,8,0,0,0,0,16H208V184a8,8,0,0,0,16,0V40A8,8,0,0,0,216,32Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M184,66H40a6,6,0,0,0-6,6V216a6,6,0,0,0,6,6H184a6,6,0,0,0,6-6V72A6,6,0,0,0,184,66Zm-6,144H46V78H178ZM222,40V184a6,6,0,0,1-12,0V46H72a6,6,0,0,1,0-12H216A6,6,0,0,1,222,40Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M184,64H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H184a8,8,0,0,0,8-8V72A8,8,0,0,0,184,64Zm-8,144H48V80H176ZM224,40V184a8,8,0,0,1-16,0V48H72a8,8,0,0,1,0-16H216A8,8,0,0,1,224,40Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M184,68H40a4,4,0,0,0-4,4V216a4,4,0,0,0,4,4H184a4,4,0,0,0,4-4V72A4,4,0,0,0,184,68Zm-4,144H44V76H180ZM220,40V184a4,4,0,0,1-8,0V44H72a4,4,0,0,1,0-8H216A4,4,0,0,1,220,40Z"}))]]),K_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M220,48V88a12,12,0,0,1-24,0V60H168a12,12,0,0,1,0-24h40A12,12,0,0,1,220,48ZM88,196H60V168a12,12,0,0,0-24,0v40a12,12,0,0,0,12,12H88a12,12,0,0,0,0-24Zm120-40a12,12,0,0,0-12,12v28H168a12,12,0,0,0,0,24h40a12,12,0,0,0,12-12V168A12,12,0,0,0,208,156ZM88,36H48A12,12,0,0,0,36,48V88a12,12,0,0,0,24,0V60H88a12,12,0,0,0,0-24Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,48V208H48V48Z",opacity:"0.2"}),D.createElement("path",{d:"M216,48V88a8,8,0,0,1-16,0V56H168a8,8,0,0,1,0-16h40A8,8,0,0,1,216,48ZM88,200H56V168a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H88a8,8,0,0,0,0-16Zm120-40a8,8,0,0,0-8,8v32H168a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V168A8,8,0,0,0,208,160ZM88,40H48a8,8,0,0,0-8,8V88a8,8,0,0,0,16,0V56H88a8,8,0,0,0,0-16Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M93.66,202.34A8,8,0,0,1,88,216H48a8,8,0,0,1-8-8V168a8,8,0,0,1,13.66-5.66ZM88,40H48a8,8,0,0,0-8,8V88a8,8,0,0,0,13.66,5.66l40-40A8,8,0,0,0,88,40ZM211.06,160.61a8,8,0,0,0-8.72,1.73l-40,40A8,8,0,0,0,168,216h40a8,8,0,0,0,8-8V168A8,8,0,0,0,211.06,160.61ZM208,40H168a8,8,0,0,0-5.66,13.66l40,40A8,8,0,0,0,216,88V48A8,8,0,0,0,208,40Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M214,48V88a6,6,0,0,1-12,0V54H168a6,6,0,0,1,0-12h40A6,6,0,0,1,214,48ZM88,202H54V168a6,6,0,0,0-12,0v40a6,6,0,0,0,6,6H88a6,6,0,0,0,0-12Zm120-40a6,6,0,0,0-6,6v34H168a6,6,0,0,0,0,12h40a6,6,0,0,0,6-6V168A6,6,0,0,0,208,162ZM88,42H48a6,6,0,0,0-6,6V88a6,6,0,0,0,12,0V54H88a6,6,0,0,0,0-12Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,48V88a8,8,0,0,1-16,0V56H168a8,8,0,0,1,0-16h40A8,8,0,0,1,216,48ZM88,200H56V168a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H88a8,8,0,0,0,0-16Zm120-40a8,8,0,0,0-8,8v32H168a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V168A8,8,0,0,0,208,160ZM88,40H48a8,8,0,0,0-8,8V88a8,8,0,0,0,16,0V56H88a8,8,0,0,0,0-16Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M212,48V88a4,4,0,0,1-8,0V52H168a4,4,0,0,1,0-8h40A4,4,0,0,1,212,48ZM88,204H52V168a4,4,0,0,0-8,0v40a4,4,0,0,0,4,4H88a4,4,0,0,0,0-8Zm120-40a4,4,0,0,0-4,4v36H168a4,4,0,0,0,0,8h40a4,4,0,0,0,4-4V168A4,4,0,0,0,208,164ZM88,44H48a4,4,0,0,0-4,4V88a4,4,0,0,0,8,0V52H88a4,4,0,0,0,0-8Z"}))]]),Y_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M156,88H100a12,12,0,0,0-12,12v56a12,12,0,0,0,12,12h56a12,12,0,0,0,12-12V100A12,12,0,0,0,156,88Zm-12,56H112V112h32Zm88-4H220V116h12a12,12,0,0,0,0-24H220V56a20,20,0,0,0-20-20H164V24a12,12,0,0,0-24,0V36H116V24a12,12,0,0,0-24,0V36H56A20,20,0,0,0,36,56V92H24a12,12,0,0,0,0,24H36v24H24a12,12,0,0,0,0,24H36v36a20,20,0,0,0,20,20H92v12a12,12,0,0,0,24,0V220h24v12a12,12,0,0,0,24,0V220h36a20,20,0,0,0,20-20V164h12a12,12,0,0,0,0-24Zm-36,56H60V60H196Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,48H56a8,8,0,0,0-8,8V200a8,8,0,0,0,8,8H200a8,8,0,0,0,8-8V56A8,8,0,0,0,200,48ZM152,152H104V104h48Z",opacity:"0.2"}),D.createElement("path",{d:"M152,96H104a8,8,0,0,0-8,8v48a8,8,0,0,0,8,8h48a8,8,0,0,0,8-8V104A8,8,0,0,0,152,96Zm-8,48H112V112h32Zm88,0H216V112h16a8,8,0,0,0,0-16H216V56a16,16,0,0,0-16-16H160V24a8,8,0,0,0-16,0V40H112V24a8,8,0,0,0-16,0V40H56A16,16,0,0,0,40,56V96H24a8,8,0,0,0,0,16H40v32H24a8,8,0,0,0,0,16H40v40a16,16,0,0,0,16,16H96v16a8,8,0,0,0,16,0V216h32v16a8,8,0,0,0,16,0V216h40a16,16,0,0,0,16-16V160h16a8,8,0,0,0,0-16Zm-32,56H56V56H200v95.87s0,.09,0,.13,0,.09,0,.13V200Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M104,104h48v48H104Zm136,48a8,8,0,0,1-8,8H216v40a16,16,0,0,1-16,16H160v16a8,8,0,0,1-16,0V216H112v16a8,8,0,0,1-16,0V216H56a16,16,0,0,1-16-16V160H24a8,8,0,0,1,0-16H40V112H24a8,8,0,0,1,0-16H40V56A16,16,0,0,1,56,40H96V24a8,8,0,0,1,16,0V40h32V24a8,8,0,0,1,16,0V40h40a16,16,0,0,1,16,16V96h16a8,8,0,0,1,0,16H216v32h16A8,8,0,0,1,240,152ZM168,96a8,8,0,0,0-8-8H96a8,8,0,0,0-8,8v64a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M152,98H104a6,6,0,0,0-6,6v48a6,6,0,0,0,6,6h48a6,6,0,0,0,6-6V104A6,6,0,0,0,152,98Zm-6,48H110V110h36Zm86,0H214V110h18a6,6,0,0,0,0-12H214V56a14,14,0,0,0-14-14H158V24a6,6,0,0,0-12,0V42H110V24a6,6,0,0,0-12,0V42H56A14,14,0,0,0,42,56V98H24a6,6,0,0,0,0,12H42v36H24a6,6,0,0,0,0,12H42v42a14,14,0,0,0,14,14H98v18a6,6,0,0,0,12,0V214h36v18a6,6,0,0,0,12,0V214h42a14,14,0,0,0,14-14V158h18a6,6,0,0,0,0-12Zm-30,54a2,2,0,0,1-2,2H56a2,2,0,0,1-2-2V56a2,2,0,0,1,2-2H200a2,2,0,0,1,2,2Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M152,96H104a8,8,0,0,0-8,8v48a8,8,0,0,0,8,8h48a8,8,0,0,0,8-8V104A8,8,0,0,0,152,96Zm-8,48H112V112h32Zm88,0H216V112h16a8,8,0,0,0,0-16H216V56a16,16,0,0,0-16-16H160V24a8,8,0,0,0-16,0V40H112V24a8,8,0,0,0-16,0V40H56A16,16,0,0,0,40,56V96H24a8,8,0,0,0,0,16H40v32H24a8,8,0,0,0,0,16H40v40a16,16,0,0,0,16,16H96v16a8,8,0,0,0,16,0V216h32v16a8,8,0,0,0,16,0V216h40a16,16,0,0,0,16-16V160h16a8,8,0,0,0,0-16Zm-32,56H56V56H200v95.87s0,.09,0,.13,0,.09,0,.13V200Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M152,100H104a4,4,0,0,0-4,4v48a4,4,0,0,0,4,4h48a4,4,0,0,0,4-4V104A4,4,0,0,0,152,100Zm-4,48H108V108h40Zm84,0H212V108h20a4,4,0,0,0,0-8H212V56a12,12,0,0,0-12-12H156V24a4,4,0,0,0-8,0V44H108V24a4,4,0,0,0-8,0V44H56A12,12,0,0,0,44,56v44H24a4,4,0,0,0,0,8H44v40H24a4,4,0,0,0,0,8H44v44a12,12,0,0,0,12,12h44v20a4,4,0,0,0,8,0V212h40v20a4,4,0,0,0,8,0V212h44a12,12,0,0,0,12-12V156h20a4,4,0,0,0,0-8Zm-28,52a4,4,0,0,1-4,4H56a4,4,0,0,1-4-4V56a4,4,0,0,1,4-4H200a4,4,0,0,1,4,4Z"}))]]),Z_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M238.16,113.89,142.1,17.83a20,20,0,0,0-28.21,0l-96,96.06a20,20,0,0,0,0,28.22l96.05,96.06h0a20,20,0,0,0,28.21,0l96-96.06a20,20,0,0,0,0-28.22ZM128,218.33,37.68,128,128,37.67,218.32,128Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M229.67,133.62l-96,96a7.94,7.94,0,0,1-11.24,0l-96-96a7.94,7.94,0,0,1,0-11.24l96.05-96a7.94,7.94,0,0,1,11.24,0l96,96.05A7.94,7.94,0,0,1,229.67,133.62Z",opacity:"0.2"}),D.createElement("path",{d:"M235.33,116.72,139.28,20.66a16,16,0,0,0-22.56,0l-96,96.06a16,16,0,0,0,0,22.56l96.05,96.06h0a16,16,0,0,0,22.56,0l96.05-96.06a16,16,0,0,0,0-22.56ZM128,224h0L32,128,128,32,224,128Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240,128a15.85,15.85,0,0,1-4.67,11.28l-96.05,96.06a16,16,0,0,1-22.56,0h0l-96-96.06a16,16,0,0,1,0-22.56l96.05-96.06a16,16,0,0,1,22.56,0l96.05,96.06A15.85,15.85,0,0,1,240,128Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M233.92,118.14,137.86,22.08a14,14,0,0,0-19.72,0L22.08,118.14a14,14,0,0,0,0,19.72l96.06,96.06h0a14,14,0,0,0,19.72,0l96-96.06a13.94,13.94,0,0,0,0-19.72Zm-8.49,11.24-96.05,96.06a2,2,0,0,1-2.76,0L30.57,129.38a2,2,0,0,1,0-2.76l96.05-96.06a2,2,0,0,1,2.76,0l96.05,96.06a2,2,0,0,1,0,2.76Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M235.33,116.72,139.28,20.66a16,16,0,0,0-22.56,0l-96,96.06a16,16,0,0,0,0,22.56l96.05,96.06h0a16,16,0,0,0,22.56,0l96.05-96.06a16,16,0,0,0,0-22.56ZM128,224h0L32,128,128,32,224,128Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232.5,119.55,136.45,23.49a12,12,0,0,0-16.9,0l-96,96.06a12,12,0,0,0,0,16.9l96.05,96.06a12,12,0,0,0,16.9,0l96.05-96.06a12,12,0,0,0,0-16.9Zm-5.66,11.24-96.05,96.06a4,4,0,0,1-5.58,0l-96-96.06a3.94,3.94,0,0,1,0-5.58l96.05-96.06a4,4,0,0,1,5.58,0l96.05,96.06a3.94,3.94,0,0,1,0,5.58Z"}))]]),X_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M144,128a16,16,0,1,1-16-16A16,16,0,0,1,144,128ZM60,112a16,16,0,1,0,16,16A16,16,0,0,0,60,112Zm136,0a16,16,0,1,0,16,16A16,16,0,0,0,196,112Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240,96v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V96A16,16,0,0,1,32,80H224A16,16,0,0,1,240,96Z",opacity:"0.2"}),D.createElement("path",{d:"M140,128a12,12,0,1,1-12-12A12,12,0,0,1,140,128Zm56-12a12,12,0,1,0,12,12A12,12,0,0,0,196,116ZM60,116a12,12,0,1,0,12,12A12,12,0,0,0,60,116Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,80H32A16,16,0,0,0,16,96v64a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V96A16,16,0,0,0,224,80ZM60,140a12,12,0,1,1,12-12A12,12,0,0,1,60,140Zm68,0a12,12,0,1,1,12-12A12,12,0,0,1,128,140Zm68,0a12,12,0,1,1,12-12A12,12,0,0,1,196,140Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M138,128a10,10,0,1,1-10-10A10,10,0,0,1,138,128ZM60,118a10,10,0,1,0,10,10A10,10,0,0,0,60,118Zm136,0a10,10,0,1,0,10,10A10,10,0,0,0,196,118Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M140,128a12,12,0,1,1-12-12A12,12,0,0,1,140,128Zm56-12a12,12,0,1,0,12,12A12,12,0,0,0,196,116ZM60,116a12,12,0,1,0,12,12A12,12,0,0,0,60,116Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M136,128a8,8,0,1,1-8-8A8,8,0,0,1,136,128Zm-76-8a8,8,0,1,0,8,8A8,8,0,0,0,60,120Zm136,0a8,8,0,1,0,8,8A8,8,0,0,0,196,120Z"}))]]),Q_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M71.51,88.49a12,12,0,0,1,17-17L116,99V24a12,12,0,0,1,24,0V99l27.51-27.52a12,12,0,0,1,17,17l-48,48a12,12,0,0,1-17,0ZM224,116H188a12,12,0,0,0,0,24h32v56H36V140H68a12,12,0,0,0,0-24H32a20,20,0,0,0-20,20v64a20,20,0,0,0,20,20H224a20,20,0,0,0,20-20V136A20,20,0,0,0,224,116Zm-20,52a16,16,0,1,0-16,16A16,16,0,0,0,204,168Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232,136v64a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V136a8,8,0,0,1,8-8H224A8,8,0,0,1,232,136Z",opacity:"0.2"}),D.createElement("path",{d:"M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H72a8,8,0,0,1,0,16H32v64H224V136H184a8,8,0,0,1,0-16h40A16,16,0,0,1,240,136Zm-117.66-2.34a8,8,0,0,0,11.32,0l48-48a8,8,0,0,0-11.32-11.32L136,108.69V24a8,8,0,0,0-16,0v84.69L85.66,74.34A8,8,0,0,0,74.34,85.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M74.34,85.66A8,8,0,0,1,85.66,74.34L120,108.69V24a8,8,0,0,1,16,0v84.69l34.34-34.35a8,8,0,0,1,11.32,11.32l-48,48a8,8,0,0,1-11.32,0ZM240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H84.4a4,4,0,0,1,2.83,1.17L111,145A24,24,0,0,0,145,145l23.8-23.8A4,4,0,0,1,171.6,120H224A16,16,0,0,1,240,136Zm-40,32a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M238,136v64a14,14,0,0,1-14,14H32a14,14,0,0,1-14-14V136a14,14,0,0,1,14-14H72a6,6,0,0,1,0,12H32a2,2,0,0,0-2,2v64a2,2,0,0,0,2,2H224a2,2,0,0,0,2-2V136a2,2,0,0,0-2-2H184a6,6,0,0,1,0-12h40A14,14,0,0,1,238,136Zm-114.24-3.76a6,6,0,0,0,8.48,0l48-48a6,6,0,0,0-8.48-8.48L134,113.51V24a6,6,0,0,0-12,0v89.51L84.24,75.76a6,6,0,0,0-8.48,8.48ZM198,168a10,10,0,1,0-10,10A10,10,0,0,0,198,168Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H72a8,8,0,0,1,0,16H32v64H224V136H184a8,8,0,0,1,0-16h40A16,16,0,0,1,240,136Zm-117.66-2.34a8,8,0,0,0,11.32,0l48-48a8,8,0,0,0-11.32-11.32L136,108.69V24a8,8,0,0,0-16,0v84.69L85.66,74.34A8,8,0,0,0,74.34,85.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M236,136v64a12,12,0,0,1-12,12H32a12,12,0,0,1-12-12V136a12,12,0,0,1,12-12H72a4,4,0,0,1,0,8H32a4,4,0,0,0-4,4v64a4,4,0,0,0,4,4H224a4,4,0,0,0,4-4V136a4,4,0,0,0-4-4H184a4,4,0,0,1,0-8h40A12,12,0,0,1,236,136Zm-110.83-5.17a4,4,0,0,0,5.66,0l48-48a4,4,0,1,0-5.66-5.66L132,118.34V24a4,4,0,0,0-8,0v94.34L82.83,77.17a4,4,0,0,0-5.66,5.66ZM196,168a8,8,0,1,0-8,8A8,8,0,0,0,196,168Z"}))]]),J_r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216.49,79.52l-56-56A12,12,0,0,0,152,20H56A20,20,0,0,0,36,40V216a20,20,0,0,0,20,20H200a20,20,0,0,0,20-20V88A12,12,0,0,0,216.49,79.52ZM160,57l23,23H160ZM60,212V44h76V92a12,12,0,0,0,12,12h48V212Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,88H152V32Z",opacity:"0.2"}),D.createElement("path",{d:"M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM152,88V44l44,44Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M212.24,83.76l-56-56A6,6,0,0,0,152,26H56A14,14,0,0,0,42,40V216a14,14,0,0,0,14,14H200a14,14,0,0,0,14-14V88A6,6,0,0,0,212.24,83.76ZM158,46.48,193.52,82H158ZM200,218H56a2,2,0,0,1-2-2V40a2,2,0,0,1,2-2h90V88a6,6,0,0,0,6,6h50V216A2,2,0,0,1,200,218Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M210.83,85.17l-56-56A4,4,0,0,0,152,28H56A12,12,0,0,0,44,40V216a12,12,0,0,0,12,12H200a12,12,0,0,0,12-12V88A4,4,0,0,0,210.83,85.17ZM156,41.65,198.34,84H156ZM200,220H56a4,4,0,0,1-4-4V40a4,4,0,0,1,4-4h92V88a4,4,0,0,0,4,4h52V216A4,4,0,0,1,200,220Z"}))]]),e2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216.49,79.51l-56-56A12,12,0,0,0,152,20H56A20,20,0,0,0,36,40v68a12,12,0,0,0,24,0V44h76V92a12,12,0,0,0,12,12h48V212H180a12,12,0,0,0,0,24h20a20,20,0,0,0,20-20V88A12,12,0,0,0,216.49,79.51ZM160,57l23,23H160Zm-4.22,139.85a24.75,24.75,0,0,1-10.95,18.06c-6,4-13.27,5.15-19.73,5.15a63.75,63.75,0,0,1-16.23-2.21,12,12,0,0,1,6.46-23.12c6.81,1.86,15,1.61,16.39.06a2.48,2.48,0,0,0,.21-.71c-1.94-1.23-6.83-2.64-9.88-3.52-5.39-1.56-11-3.18-15.75-6.27-7.62-4.92-11.21-12.45-10.11-21.2a24.45,24.45,0,0,1,10.69-17.75c6.06-4.09,14.17-5.84,24.1-5.18A68.53,68.53,0,0,1,143,142a12,12,0,0,1-6.1,23.21c-6.36-1.63-13.62-1.51-16.07-.33a79.5,79.5,0,0,0,7.91,2.59c5.48,1.58,11.68,3.37,16.8,6.82C153.33,179.55,157,187.55,155.78,196.82ZM84,152v38a30,30,0,0,1-60,0,12,12,0,0,1,24,0,6,6,0,0,0,12,0V152a12,12,0,0,1,24,0Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,88H152V32Z",opacity:"0.2"}),D.createElement("path",{d:"M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v72a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48V216H176a8,8,0,0,0,0,16h24a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160Zm-12.19,145a20.82,20.82,0,0,1-9.19,15.23C133.43,215,127,216,121.13,216A61.14,61.14,0,0,1,106,214a8,8,0,1,1,4.3-15.41c4.38,1.2,15,2.7,19.55-.36.88-.59,1.83-1.52,2.14-3.93.35-2.67-.71-4.1-12.78-7.59-9.35-2.7-25-7.23-23-23.11a20.56,20.56,0,0,1,9-14.95c11.84-8,30.71-3.31,32.83-2.76a8,8,0,0,1-4.07,15.48c-4.49-1.17-15.23-2.56-19.83.56a4.54,4.54,0,0,0-2,3.67c-.12.9-.14,1.09,1.11,1.9,2.31,1.49,6.45,2.68,10.45,3.84C133.49,174.17,150.05,179,147.81,196.31ZM80,152v38a26,26,0,0,1-52,0,8,8,0,0,1,16,0,10,10,0,0,0,20,0V152a8,8,0,0,1,16,0Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v76a4,4,0,0,0,4,4H164a4,4,0,0,1,4,4V228a4,4,0,0,0,4,4h28a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM152,88V44l44,44Zm-4.19,108.31a20.82,20.82,0,0,1-9.19,15.23C133.43,215,127,216,121.13,216a61.34,61.34,0,0,1-15.19-2,8,8,0,0,1,4.31-15.41c4.38,1.2,15,2.7,19.55-.36.88-.59,1.83-1.52,2.14-3.93.34-2.67-.72-4.1-12.78-7.59-9.35-2.7-25-7.23-23-23.11a20.58,20.58,0,0,1,9-14.95c11.85-8,30.72-3.31,32.84-2.76a8,8,0,0,1-4.07,15.48c-4.49-1.17-15.23-2.56-19.83.56a4.57,4.57,0,0,0-2,3.67c-.11.9-.13,1.09,1.12,1.9,2.31,1.49,6.45,2.68,10.45,3.84C133.49,174.17,150,179,147.81,196.31ZM80,152v37.41c0,14.22-11.18,26.26-25.41,26.58A26,26,0,0,1,28,190.37,8.17,8.17,0,0,1,35.31,182,8,8,0,0,1,44,190.22a8.89,8.89,0,0,0,4,8c7.85,4.82,16-.75,16-8.2V152.27A8.17,8.17,0,0,1,71.47,144,8,8,0,0,1,80,152Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M212.24,83.76l-56-56A6,6,0,0,0,152,26H56A14,14,0,0,0,42,40v72a6,6,0,0,0,12,0V40a2,2,0,0,1,2-2h90V88a6,6,0,0,0,6,6h50V216a2,2,0,0,1-2,2H176a6,6,0,0,0,0,12h24a14,14,0,0,0,14-14V88A6,6,0,0,0,212.24,83.76ZM158,46.48,193.52,82H158ZM145.83,196.06a18.89,18.89,0,0,1-8.31,13.81c-4.82,3.19-10.87,4.14-16.36,4.14a58.89,58.89,0,0,1-14.68-2,6,6,0,0,1,3.23-11.56c3.71,1,15.58,3.11,21.19-.62a6.85,6.85,0,0,0,3-5.34c.58-4.43-2.08-6.26-14.2-9.76-9.31-2.69-23.37-6.75-21.57-20.94a18.61,18.61,0,0,1,8.08-13.54c11.11-7.49,29.18-3,31.21-2.48a6,6,0,0,1-3.06,11.6c-3.78-1-15.85-3-21.45.84a6.59,6.59,0,0,0-2.88,5.08c-.41,3.22,2.14,4.78,13,7.91C132.92,176.09,147.84,180.4,145.83,196.06ZM78,152v38a24,24,0,0,1-48,0,6,6,0,0,1,12,0,12,12,0,0,0,24,0V152a6,6,0,0,1,12,0Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v72a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48V216H176a8,8,0,0,0,0,16h24a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160Zm-12.19,145a20.82,20.82,0,0,1-9.19,15.23C133.43,215,127,216,121.13,216a61.34,61.34,0,0,1-15.19-2,8,8,0,0,1,4.31-15.41c4.38,1.2,15,2.7,19.55-.36.88-.59,1.83-1.52,2.14-3.93.34-2.67-.71-4.1-12.78-7.59-9.35-2.7-25-7.23-23-23.11a20.56,20.56,0,0,1,9-14.95c11.84-8,30.71-3.31,32.83-2.76a8,8,0,0,1-4.07,15.48c-4.49-1.17-15.23-2.56-19.83.56a4.54,4.54,0,0,0-2,3.67c-.12.9-.14,1.09,1.11,1.9,2.31,1.49,6.45,2.68,10.45,3.84C133.49,174.17,150.05,179,147.81,196.31ZM80,152v38a26,26,0,0,1-52,0,8,8,0,0,1,16,0,10,10,0,0,0,20,0V152a8,8,0,0,1,16,0Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M210.83,85.17l-56-56A4,4,0,0,0,152,28H56A12,12,0,0,0,44,40v72a4,4,0,0,0,8,0V40a4,4,0,0,1,4-4h92V88a4,4,0,0,0,4,4h52V216a4,4,0,0,1-4,4H176a4,4,0,0,0,0,8h24a12,12,0,0,0,12-12V88A4,4,0,0,0,210.83,85.17ZM156,41.65,198.34,84H156ZM143.84,195.8a17,17,0,0,1-7.43,12.41c-4.39,2.91-10,3.77-15.22,3.77A57.89,57.89,0,0,1,107,210.11a4,4,0,0,1,2.15-7.7c4.22,1.17,16.56,3.29,22.83-.88a8.94,8.94,0,0,0,3.91-6.75c.83-6.45-4.38-8.69-15.64-11.94-9.68-2.8-21.72-6.28-20.14-18.77a16.66,16.66,0,0,1,7.22-12.13c4.56-3.07,11-4.36,19.1-3.82a61.33,61.33,0,0,1,10.48,1.61,4,4,0,0,1-2.05,7.74c-4.29-1.13-16.81-3.12-23.06,1.11a8.51,8.51,0,0,0-3.75,6.49c-.66,5.17,3.89,7,14.42,10.08C132.26,178,145.64,181.84,143.84,195.8ZM76,152v38a22,22,0,0,1-44,0,4,4,0,0,1,8,0,14,14,0,0,0,28,0V152a4,4,0,0,1,8,0Z"}))]]),t2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216.49,79.52l-56-56A12,12,0,0,0,152,20H56A20,20,0,0,0,36,40V216a20,20,0,0,0,20,20H200a20,20,0,0,0,20-20V88A12,12,0,0,0,216.49,79.52ZM183,80H160V57ZM60,212V44h76V92a12,12,0,0,0,12,12h48V212Zm96.48-48.49a36,36,0,1,0-17,17l12,12a12,12,0,0,0,17-17ZM112,148a12,12,0,1,1,12,12A12,12,0,0,1,112,148Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,88H152V32Z",opacity:"0.2"}),D.createElement("path",{d:"M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Zm-45.54-48.85a36.05,36.05,0,1,0-11.31,11.31l11.19,11.2a8,8,0,0,0,11.32-11.32ZM104,148a20,20,0,1,1,20,20A20,20,0,0,1,104,148Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M144,148a20,20,0,1,1-20-20A20,20,0,0,1,144,148Zm72-60V216a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V40A16,16,0,0,1,56,24h96a8,8,0,0,1,5.66,2.34l56,56A8,8,0,0,1,216,88Zm-50.34,90.34-11.2-11.19a36.05,36.05,0,1,0-11.31,11.31l11.19,11.2a8,8,0,0,0,11.32-11.32ZM196,88,152,44V88Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M212.24,83.76l-56-56A6,6,0,0,0,152,26H56A14,14,0,0,0,42,40V216a14,14,0,0,0,14,14H200a14,14,0,0,0,14-14V88A6,6,0,0,0,212.24,83.76ZM158,46.48,193.52,82H158ZM200,218H56a2,2,0,0,1-2-2V40a2,2,0,0,1,2-2h90V88a6,6,0,0,0,6,6h50V216A2,2,0,0,1,200,218Zm-48.11-50.59a34.05,34.05,0,1,0-8.48,8.48l12.35,12.35a6,6,0,0,0,8.48-8.48ZM102,148a22,22,0,1,1,22,22A22,22,0,0,1,102,148Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Zm-45.54-48.85a36.05,36.05,0,1,0-11.31,11.31l11.19,11.2a8,8,0,0,0,11.32-11.32ZM104,148a20,20,0,1,1,20,20A20,20,0,0,1,104,148Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M210.83,85.17l-56-56A4,4,0,0,0,152,28H56A12,12,0,0,0,44,40V216a12,12,0,0,0,12,12H200a12,12,0,0,0,12-12V88A4,4,0,0,0,210.83,85.17ZM156,41.65,198.34,84H156ZM200,220H56a4,4,0,0,1-4-4V40a4,4,0,0,1,4-4h92V88a4,4,0,0,0,4,4h52V216A4,4,0,0,1,200,220Zm-50.74-52.39a32.05,32.05,0,1,0-5.65,5.65l13.56,13.57a4,4,0,0,0,5.66-5.66ZM100,148a24,24,0,1,1,24,24A24,24,0,0,1,100,148Z"}))]]),n2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216.49,79.52l-56-56A12,12,0,0,0,152,20H56A20,20,0,0,0,36,40V216a20,20,0,0,0,20,20H200a20,20,0,0,0,20-20V88A12,12,0,0,0,216.49,79.52ZM160,57l23,23H160ZM60,212V44h76V92a12,12,0,0,0,12,12h48V212Zm112-80a12,12,0,0,1-12,12H96a12,12,0,0,1,0-24h64A12,12,0,0,1,172,132Zm0,40a12,12,0,0,1-12,12H96a12,12,0,0,1,0-24h64A12,12,0,0,1,172,172Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,88H152V32Z",opacity:"0.2"}),D.createElement("path",{d:"M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Zm-32-80a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,136Zm0,32a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,168Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,176H96a8,8,0,0,1,0-16h64a8,8,0,0,1,0,16Zm0-32H96a8,8,0,0,1,0-16h64a8,8,0,0,1,0,16Zm-8-56V44l44,44Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M212.24,83.76l-56-56A6,6,0,0,0,152,26H56A14,14,0,0,0,42,40V216a14,14,0,0,0,14,14H200a14,14,0,0,0,14-14V88A6,6,0,0,0,212.24,83.76ZM158,46.48,193.52,82H158ZM200,218H56a2,2,0,0,1-2-2V40a2,2,0,0,1,2-2h90V88a6,6,0,0,0,6,6h50V216A2,2,0,0,1,200,218Zm-34-82a6,6,0,0,1-6,6H96a6,6,0,0,1,0-12h64A6,6,0,0,1,166,136Zm0,32a6,6,0,0,1-6,6H96a6,6,0,0,1,0-12h64A6,6,0,0,1,166,168Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Zm-32-80a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,136Zm0,32a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,168Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M210.83,85.17l-56-56A4,4,0,0,0,152,28H56A12,12,0,0,0,44,40V216a12,12,0,0,0,12,12H200a12,12,0,0,0,12-12V88A4,4,0,0,0,210.83,85.17ZM156,41.65,198.34,84H156ZM200,220H56a4,4,0,0,1-4-4V40a4,4,0,0,1,4-4h92V88a4,4,0,0,0,4,4h52V216A4,4,0,0,1,200,220Zm-36-84a4,4,0,0,1-4,4H96a4,4,0,0,1,0-8h64A4,4,0,0,1,164,136Zm0,32a4,4,0,0,1-4,4H96a4,4,0,0,1,0-8h64A4,4,0,0,1,164,168Z"}))]]),i2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M220.49,59.51l-40-40A12,12,0,0,0,172,16H92A20,20,0,0,0,72,36V56H56A20,20,0,0,0,36,76V216a20,20,0,0,0,20,20H164a20,20,0,0,0,20-20V196h20a20,20,0,0,0,20-20V68A12,12,0,0,0,220.49,59.51ZM160,212H60V80h67l33,33Zm40-40H184V108a12,12,0,0,0-3.51-8.49l-40-40A12,12,0,0,0,132,56H96V40h71l33,33Zm-56-28a12,12,0,0,1-12,12H88a12,12,0,0,1,0-24h44A12,12,0,0,1,144,144Zm0,40a12,12,0,0,1-12,12H88a12,12,0,0,1,0-24h44A12,12,0,0,1,144,184Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,72V184a8,8,0,0,1-8,8H176V104L136,64H80V40a8,8,0,0,1,8-8h80Z",opacity:"0.2"}),D.createElement("path",{d:"M213.66,66.34l-40-40A8,8,0,0,0,168,24H88A16,16,0,0,0,72,40V56H56A16,16,0,0,0,40,72V216a16,16,0,0,0,16,16H168a16,16,0,0,0,16-16V200h16a16,16,0,0,0,16-16V72A8,8,0,0,0,213.66,66.34ZM168,216H56V72h76.69L168,107.31v84.53c0,.06,0,.11,0,.16s0,.1,0,.16V216Zm32-32H184V104a8,8,0,0,0-2.34-5.66l-40-40A8,8,0,0,0,136,56H88V40h76.69L200,75.31Zm-56-32a8,8,0,0,1-8,8H88a8,8,0,0,1,0-16h48A8,8,0,0,1,144,152Zm0,32a8,8,0,0,1-8,8H88a8,8,0,0,1,0-16h48A8,8,0,0,1,144,184Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,66.34l-40-40A8,8,0,0,0,168,24H88A16,16,0,0,0,72,40V56H56A16,16,0,0,0,40,72V216a16,16,0,0,0,16,16H168a16,16,0,0,0,16-16V200h16a16,16,0,0,0,16-16V72A8,8,0,0,0,213.66,66.34ZM136,192H88a8,8,0,0,1,0-16h48a8,8,0,0,1,0,16Zm0-32H88a8,8,0,0,1,0-16h48a8,8,0,0,1,0,16Zm64,24H184V104a8,8,0,0,0-2.34-5.66l-40-40A8,8,0,0,0,136,56H88V40h76.69L200,75.31Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M212.24,67.76l-40-40A6,6,0,0,0,168,26H88A14,14,0,0,0,74,40V58H56A14,14,0,0,0,42,72V216a14,14,0,0,0,14,14H168a14,14,0,0,0,14-14V198h18a14,14,0,0,0,14-14V72A6,6,0,0,0,212.24,67.76ZM170,216a2,2,0,0,1-2,2H56a2,2,0,0,1-2-2V72a2,2,0,0,1,2-2h77.51L170,106.49Zm32-32a2,2,0,0,1-2,2H182V104a6,6,0,0,0-1.76-4.24l-40-40A6,6,0,0,0,136,58H86V40a2,2,0,0,1,2-2h77.51L202,74.49Zm-60-32a6,6,0,0,1-6,6H88a6,6,0,0,1,0-12h48A6,6,0,0,1,142,152Zm0,32a6,6,0,0,1-6,6H88a6,6,0,0,1,0-12h48A6,6,0,0,1,142,184Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.66,66.34l-40-40A8,8,0,0,0,168,24H88A16,16,0,0,0,72,40V56H56A16,16,0,0,0,40,72V216a16,16,0,0,0,16,16H168a16,16,0,0,0,16-16V200h16a16,16,0,0,0,16-16V72A8,8,0,0,0,213.66,66.34ZM168,216H56V72h76.69L168,107.31v84.53c0,.06,0,.11,0,.16s0,.1,0,.16V216Zm32-32H184V104a8,8,0,0,0-2.34-5.66l-40-40A8,8,0,0,0,136,56H88V40h76.69L200,75.31Zm-56-32a8,8,0,0,1-8,8H88a8,8,0,0,1,0-16h48A8,8,0,0,1,144,152Zm0,32a8,8,0,0,1-8,8H88a8,8,0,0,1,0-16h48A8,8,0,0,1,144,184Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M210.83,69.17l-40-40A4,4,0,0,0,168,28H88A12,12,0,0,0,76,40V60H56A12,12,0,0,0,44,72V216a12,12,0,0,0,12,12H168a12,12,0,0,0,12-12V196h20a12,12,0,0,0,12-12V72A4,4,0,0,0,210.83,69.17ZM172,216a4,4,0,0,1-4,4H56a4,4,0,0,1-4-4V72a4,4,0,0,1,4-4h78.34L172,105.66Zm32-32a4,4,0,0,1-4,4H180V104a4,4,0,0,0-1.17-2.83l-40-40A4,4,0,0,0,136,60H84V40a4,4,0,0,1,4-4h78.34L204,73.66Zm-64-32a4,4,0,0,1-4,4H88a4,4,0,0,1,0-8h48A4,4,0,0,1,140,152Zm0,32a4,4,0,0,1-4,4H88a4,4,0,0,1,0-8h48A4,4,0,0,1,140,184Z"}))]]),r2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M40.14,46.88A12,12,0,0,0,36,56V224a12,12,0,0,0,24,0V181.72c22.84-17.12,42.1-9.12,70.68,5,16.23,8,34.74,17.2,54.8,17.2,14.72,0,30.28-4.94,46.38-18.88A12,12,0,0,0,236,176V56a12,12,0,0,0-19.86-9.07c-24.71,21.41-44.53,13.31-74.82-1.68C113.19,31.27,78.17,13.94,40.14,46.88ZM212,170.26c-22.84,17.13-42.1,9.11-70.68-5C118.16,153.76,90.33,140,60,153.87V61.69c22.84-17.12,42.1-9.12,70.68,5,16.23,8,34.74,17.2,54.8,17.2A63,63,0,0,0,212,78.08Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,56V176c-64,55.43-112-55.43-176,0V56C112,.57,160,111.43,224,56Z",opacity:"0.2"}),D.createElement("path",{d:"M42.76,50A8,8,0,0,0,40,56V224a8,8,0,0,0,16,0V179.77c26.79-21.16,49.87-9.75,76.45,3.41,16.4,8.11,34.06,16.85,53,16.85,13.93,0,28.54-4.75,43.82-18a8,8,0,0,0,2.76-6V56A8,8,0,0,0,218.76,50c-28,24.23-51.72,12.49-79.21-1.12C111.07,34.76,78.78,18.79,42.76,50ZM216,172.25c-26.79,21.16-49.87,9.74-76.45-3.41-25-12.35-52.81-26.13-83.55-8.4V59.79c26.79-21.16,49.87-9.75,76.45,3.4,25,12.35,52.82,26.13,83.55,8.4Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232,56V176a8,8,0,0,1-2.76,6c-15.28,13.23-29.89,18-43.82,18-18.91,0-36.57-8.74-53-16.85C105.87,170,82.79,158.61,56,179.77V224a8,8,0,0,1-16,0V56a8,8,0,0,1,2.77-6h0c36-31.18,68.31-15.21,96.79-1.12C167,62.46,190.79,74.2,218.76,50A8,8,0,0,1,232,56Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M44.08,51.37A6,6,0,0,0,42,55.9V224a6,6,0,0,0,12,0V178.78c28.08-22.79,51.88-11,79.34,2.57,16.12,8,33.49,16.58,52,16.58,13.57,0,27.76-4.6,42.56-17.42A6,6,0,0,0,230,176V55.9a6,6,0,0,0-9.93-4.54c-29,25.12-53.28,13.09-81.41-.84C110.77,36.71,79,21.16,44.08,51.37ZM218,173.17c-28.08,22.8-51.88,11-79.34-2.58C113.4,158.08,85.09,144.07,54,164V58.72c28.08-22.8,51.88-11,79.34,2.56C158.6,73.79,186.91,87.8,218,67.91Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M42.76,50A8,8,0,0,0,40,56V224a8,8,0,0,0,16,0V179.77c26.79-21.16,49.87-9.75,76.45,3.41,16.4,8.11,34.06,16.85,53,16.85,13.93,0,28.54-4.75,43.82-18a8,8,0,0,0,2.76-6V56A8,8,0,0,0,218.76,50c-28,24.23-51.72,12.49-79.21-1.12C111.07,34.76,78.78,18.79,42.76,50ZM216,172.25c-26.79,21.16-49.87,9.74-76.45-3.41-25-12.35-52.81-26.13-83.55-8.4V59.79c26.79-21.16,49.87-9.75,76.45,3.4,25,12.35,52.82,26.13,83.55,8.4Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M45.39,53.06a4,4,0,0,0-1.39,3V224a4,4,0,0,0,8,0V177.87c29.41-24.39,55.08-11.69,82.23,1.73,16.5,8.17,33.33,16.5,51.13,16.5,13.14,0,26.81-4.55,41.26-17.06a4,4,0,0,0,1.38-3v-120a4,4,0,0,0-6.62-3c-30,26-56,13.07-83.61-.57C109.07,38.28,79.4,23.62,45.39,53.06ZM220,174.17c-29.41,24.4-55.08,11.7-82.23-1.73-26.82-13.27-54.5-27-85.77-4.66V57.92c29.41-24.4,55.08-11.7,82.23,1.73,26.82,13.27,54.5,27,85.77,4.66Z"}))]]),s2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,68H132L105.33,48a20.12,20.12,0,0,0-12-4H40A20,20,0,0,0,20,64V200a20,20,0,0,0,20,20H216.89A19.13,19.13,0,0,0,236,200.89V88A20,20,0,0,0,216,68Zm-4,128H44V68H92l28.8,21.6A12,12,0,0,0,128,92h84Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,88V200.89a7.11,7.11,0,0,1-7.11,7.11H40a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H93.33a8,8,0,0,1,4.8,1.6L128,80h88A8,8,0,0,1,224,88Z",opacity:"0.2"}),D.createElement("path",{d:"M216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72Zm0,128H40V64H93.33L123.2,86.4A8,8,0,0,0,128,88h88Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232,88V200.89A15.13,15.13,0,0,1,216.89,216H40a16,16,0,0,1-16-16V64A16,16,0,0,1,40,48H93.33a16.12,16.12,0,0,1,9.6,3.2L130.67,72H216A16,16,0,0,1,232,88Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,74H130L101.73,52.8a14,14,0,0,0-8.4-2.8H40A14,14,0,0,0,26,64V200a14,14,0,0,0,14,14H216.89A13.12,13.12,0,0,0,230,200.89V88A14,14,0,0,0,216,74Zm2,126.89a1.11,1.11,0,0,1-1.11,1.11H40a2,2,0,0,1-2-2V64a2,2,0,0,1,2-2H93.33a2,2,0,0,1,1.2.4L124.4,84.8A6,6,0,0,0,128,86h88a2,2,0,0,1,2,2Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72Zm0,128H40V64H93.33L123.2,86.4A8,8,0,0,0,128,88h88Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,76H129.33l-28.8-21.6a12.05,12.05,0,0,0-7.2-2.4H40A12,12,0,0,0,28,64V200a12,12,0,0,0,12,12H216.89A11.12,11.12,0,0,0,228,200.89V88A12,12,0,0,0,216,76Zm4,124.89a3.12,3.12,0,0,1-3.11,3.11H40a4,4,0,0,1-4-4V64a4,4,0,0,1,4-4H93.33a4,4,0,0,1,2.4.8L125.6,83.2a4,4,0,0,0,2.4.8h88a4,4,0,0,1,4,4Z"}))]]),o2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M212,40a12,12,0,0,1-12,12H170.71A20,20,0,0,0,151,68.42L142.38,116H184a12,12,0,0,1,0,24H138l-9.44,51.87A44,44,0,0,1,85.29,228H56a12,12,0,0,1,0-24H85.29A20,20,0,0,0,105,187.58L113.62,140H72a12,12,0,0,1,0-24h46l9.44-51.87A44,44,0,0,1,170.71,28H200A12,12,0,0,1,212,40Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,40V200a16,16,0,0,1-16,16H56V56A16,16,0,0,1,72,40Z",opacity:"0.2"}),D.createElement("path",{d:"M208,40a8,8,0,0,1-8,8H170.71a24,24,0,0,0-23.62,19.71L137.59,120H184a8,8,0,0,1,0,16H134.68l-10,55.16A40,40,0,0,1,85.29,224H56a8,8,0,0,1,0-16H85.29a24,24,0,0,0,23.62-19.71l9.5-52.29H72a8,8,0,0,1,0-16h49.32l10-55.16A40,40,0,0,1,170.71,32H200A8,8,0,0,1,208,40Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM176,72H159.92a16,16,0,0,0-15.73,13l-6.55,35H168a8,8,0,0,1,0,16H134.64l-7.11,37.9A32,32,0,0,1,96.08,200H80a8,8,0,0,1,0-16H96.08A16,16,0,0,0,111.81,171L118.36,136H88a8,8,0,0,1,0-16h33.36l7.11-37.9A32,32,0,0,1,159.92,56H176a8,8,0,0,1,0,16Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M206,40a6,6,0,0,1-6,6H170.71a26,26,0,0,0-25.58,21.35L135.19,122H184a6,6,0,0,1,0,12H133l-10.33,56.8A38,38,0,0,1,85.29,222H56a6,6,0,0,1,0-12H85.29a26,26,0,0,0,25.58-21.35L120.81,134H72a6,6,0,0,1,0-12h51l10.33-56.8A38,38,0,0,1,170.71,34H200A6,6,0,0,1,206,40Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,40a8,8,0,0,1-8,8H170.71a24,24,0,0,0-23.62,19.71L137.59,120H184a8,8,0,0,1,0,16H134.68l-10,55.16A40,40,0,0,1,85.29,224H56a8,8,0,0,1,0-16H85.29a24,24,0,0,0,23.62-19.71l9.5-52.29H72a8,8,0,0,1,0-16h49.32l10-55.16A40,40,0,0,1,170.71,32H200A8,8,0,0,1,208,40Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M204,40a4,4,0,0,1-4,4H170.71a28,28,0,0,0-27.55,23l-10.37,57H184a4,4,0,0,1,0,8H131.34l-10.63,58.44A36,36,0,0,1,85.29,220H56a4,4,0,0,1,0-8H85.29a28,28,0,0,0,27.55-23l10.37-57H72a4,4,0,0,1,0-8h52.66l10.63-58.44A36,36,0,0,1,170.71,36H200A4,4,0,0,1,204,40Z"}))]]),a2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,76a52,52,0,1,0,52,52A52.06,52.06,0,0,0,128,76Zm0,80a28,28,0,1,1,28-28A28,28,0,0,1,128,156Zm92-27.21v-1.58l14-17.51a12,12,0,0,0,2.23-10.59A111.75,111.75,0,0,0,225,71.89,12,12,0,0,0,215.89,66L193.61,63.5l-1.11-1.11L190,40.1A12,12,0,0,0,184.11,31a111.67,111.67,0,0,0-27.23-11.27A12,12,0,0,0,146.3,22L128.79,36h-1.58L109.7,22a12,12,0,0,0-10.59-2.23A111.75,111.75,0,0,0,71.89,31.05,12,12,0,0,0,66,40.11L63.5,62.39,62.39,63.5,40.1,66A12,12,0,0,0,31,71.89,111.67,111.67,0,0,0,19.77,99.12,12,12,0,0,0,22,109.7l14,17.51v1.58L22,146.3a12,12,0,0,0-2.23,10.59,111.75,111.75,0,0,0,11.29,27.22A12,12,0,0,0,40.11,190l22.28,2.48,1.11,1.11L66,215.9A12,12,0,0,0,71.89,225a111.67,111.67,0,0,0,27.23,11.27A12,12,0,0,0,109.7,234l17.51-14h1.58l17.51,14a12,12,0,0,0,10.59,2.23A111.75,111.75,0,0,0,184.11,225a12,12,0,0,0,5.91-9.06l2.48-22.28,1.11-1.11L215.9,190a12,12,0,0,0,9.06-5.91,111.67,111.67,0,0,0,11.27-27.23A12,12,0,0,0,234,146.3Zm-24.12-4.89a70.1,70.1,0,0,1,0,8.2,12,12,0,0,0,2.61,8.22l12.84,16.05A86.47,86.47,0,0,1,207,166.86l-20.43,2.27a12,12,0,0,0-7.65,4,69,69,0,0,1-5.8,5.8,12,12,0,0,0-4,7.65L166.86,207a86.47,86.47,0,0,1-10.49,4.35l-16.05-12.85a12,12,0,0,0-7.5-2.62c-.24,0-.48,0-.72,0a70.1,70.1,0,0,1-8.2,0,12.06,12.06,0,0,0-8.22,2.6L99.63,211.33A86.47,86.47,0,0,1,89.14,207l-2.27-20.43a12,12,0,0,0-4-7.65,69,69,0,0,1-5.8-5.8,12,12,0,0,0-7.65-4L49,166.86a86.47,86.47,0,0,1-4.35-10.49l12.84-16.05a12,12,0,0,0,2.61-8.22,70.1,70.1,0,0,1,0-8.2,12,12,0,0,0-2.61-8.22L44.67,99.63A86.47,86.47,0,0,1,49,89.14l20.43-2.27a12,12,0,0,0,7.65-4,69,69,0,0,1,5.8-5.8,12,12,0,0,0,4-7.65L89.14,49a86.47,86.47,0,0,1,10.49-4.35l16.05,12.85a12.06,12.06,0,0,0,8.22,2.6,70.1,70.1,0,0,1,8.2,0,12,12,0,0,0,8.22-2.6l16.05-12.85A86.47,86.47,0,0,1,166.86,49l2.27,20.43a12,12,0,0,0,4,7.65,69,69,0,0,1,5.8,5.8,12,12,0,0,0,7.65,4L207,89.14a86.47,86.47,0,0,1,4.35,10.49l-12.84,16.05A12,12,0,0,0,195.88,123.9Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M207.86,123.18l16.78-21a99.14,99.14,0,0,0-10.07-24.29l-26.7-3a81,81,0,0,0-6.81-6.81l-3-26.71a99.43,99.43,0,0,0-24.3-10l-21,16.77a81.59,81.59,0,0,0-9.64,0l-21-16.78A99.14,99.14,0,0,0,77.91,41.43l-3,26.7a81,81,0,0,0-6.81,6.81l-26.71,3a99.43,99.43,0,0,0-10,24.3l16.77,21a81.59,81.59,0,0,0,0,9.64l-16.78,21a99.14,99.14,0,0,0,10.07,24.29l26.7,3a81,81,0,0,0,6.81,6.81l3,26.71a99.43,99.43,0,0,0,24.3,10l21-16.77a81.59,81.59,0,0,0,9.64,0l21,16.78a99.14,99.14,0,0,0,24.29-10.07l3-26.7a81,81,0,0,0,6.81-6.81l26.71-3a99.43,99.43,0,0,0,10-24.3l-16.77-21A81.59,81.59,0,0,0,207.86,123.18ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z",opacity:"0.2"}),D.createElement("path",{d:"M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm88-29.84q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.6,107.6,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.29,107.29,0,0,0-26.25-10.86,8,8,0,0,0-7.06,1.48L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.6,107.6,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06Zm-16.1-6.5a73.93,73.93,0,0,1,0,8.68,8,8,0,0,0,1.74,5.48l14.19,17.73a91.57,91.57,0,0,1-6.23,15L187,173.11a8,8,0,0,0-5.1,2.64,74.11,74.11,0,0,1-6.14,6.14,8,8,0,0,0-2.64,5.1l-2.51,22.58a91.32,91.32,0,0,1-15,6.23l-17.74-14.19a8,8,0,0,0-5-1.75h-.48a73.93,73.93,0,0,1-8.68,0,8.06,8.06,0,0,0-5.48,1.74L100.45,215.8a91.57,91.57,0,0,1-15-6.23L82.89,187a8,8,0,0,0-2.64-5.1,74.11,74.11,0,0,1-6.14-6.14,8,8,0,0,0-5.1-2.64L46.43,170.6a91.32,91.32,0,0,1-6.23-15l14.19-17.74a8,8,0,0,0,1.74-5.48,73.93,73.93,0,0,1,0-8.68,8,8,0,0,0-1.74-5.48L40.2,100.45a91.57,91.57,0,0,1,6.23-15L69,82.89a8,8,0,0,0,5.1-2.64,74.11,74.11,0,0,1,6.14-6.14A8,8,0,0,0,82.89,69L85.4,46.43a91.32,91.32,0,0,1,15-6.23l17.74,14.19a8,8,0,0,0,5.48,1.74,73.93,73.93,0,0,1,8.68,0,8.06,8.06,0,0,0,5.48-1.74L155.55,40.2a91.57,91.57,0,0,1,15,6.23L173.11,69a8,8,0,0,0,2.64,5.1,74.11,74.11,0,0,1,6.14,6.14,8,8,0,0,0,5.1,2.64l22.58,2.51a91.32,91.32,0,0,1,6.23,15l-14.19,17.74A8,8,0,0,0,199.87,123.66Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,130.16q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.6,107.6,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.29,107.29,0,0,0-26.25-10.86,8,8,0,0,0-7.06,1.48L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.6,107.6,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,82a46,46,0,1,0,46,46A46.06,46.06,0,0,0,128,82Zm0,80a34,34,0,1,1,34-34A34,34,0,0,1,128,162ZM214,130.84c.06-1.89.06-3.79,0-5.68L229.33,106a6,6,0,0,0,1.11-5.29A105.34,105.34,0,0,0,219.76,74.9a6,6,0,0,0-4.53-3l-24.45-2.71q-1.93-2.07-4-4l-2.72-24.46a6,6,0,0,0-3-4.53,105.65,105.65,0,0,0-25.77-10.66A6,6,0,0,0,150,26.68l-19.2,15.37c-1.89-.06-3.79-.06-5.68,0L106,26.67a6,6,0,0,0-5.29-1.11A105.34,105.34,0,0,0,74.9,36.24a6,6,0,0,0-3,4.53L69.23,65.22q-2.07,1.94-4,4L40.76,72a6,6,0,0,0-4.53,3,105.65,105.65,0,0,0-10.66,25.77A6,6,0,0,0,26.68,106l15.37,19.2c-.06,1.89-.06,3.79,0,5.68L26.67,150.05a6,6,0,0,0-1.11,5.29A105.34,105.34,0,0,0,36.24,181.1a6,6,0,0,0,4.53,3l24.45,2.71q1.94,2.07,4,4L72,215.24a6,6,0,0,0,3,4.53,105.65,105.65,0,0,0,25.77,10.66,6,6,0,0,0,5.29-1.11L125.16,214c1.89.06,3.79.06,5.68,0l19.21,15.38a6,6,0,0,0,3.75,1.31,6.2,6.2,0,0,0,1.54-.2,105.34,105.34,0,0,0,25.76-10.68,6,6,0,0,0,3-4.53l2.71-24.45q2.07-1.93,4-4l24.46-2.72a6,6,0,0,0,4.53-3,105.49,105.49,0,0,0,10.66-25.77,6,6,0,0,0-1.11-5.29Zm-3.1,41.63-23.64,2.63a6,6,0,0,0-3.82,2,75.14,75.14,0,0,1-6.31,6.31,6,6,0,0,0-2,3.82l-2.63,23.63A94.28,94.28,0,0,1,155.14,218l-18.57-14.86a6,6,0,0,0-3.75-1.31h-.36a78.07,78.07,0,0,1-8.92,0,6,6,0,0,0-4.11,1.3L100.87,218a94.13,94.13,0,0,1-17.34-7.17L80.9,187.21a6,6,0,0,0-2-3.82,75.14,75.14,0,0,1-6.31-6.31,6,6,0,0,0-3.82-2l-23.63-2.63A94.28,94.28,0,0,1,38,155.14l14.86-18.57a6,6,0,0,0,1.3-4.11,78.07,78.07,0,0,1,0-8.92,6,6,0,0,0-1.3-4.11L38,100.87a94.13,94.13,0,0,1,7.17-17.34L68.79,80.9a6,6,0,0,0,3.82-2,75.14,75.14,0,0,1,6.31-6.31,6,6,0,0,0,2-3.82l2.63-23.63A94.28,94.28,0,0,1,100.86,38l18.57,14.86a6,6,0,0,0,4.11,1.3,78.07,78.07,0,0,1,8.92,0,6,6,0,0,0,4.11-1.3L155.13,38a94.13,94.13,0,0,1,17.34,7.17l2.63,23.64a6,6,0,0,0,2,3.82,75.14,75.14,0,0,1,6.31,6.31,6,6,0,0,0,3.82,2l23.63,2.63A94.28,94.28,0,0,1,218,100.86l-14.86,18.57a6,6,0,0,0-1.3,4.11,78.07,78.07,0,0,1,0,8.92,6,6,0,0,0,1.3,4.11L218,155.13A94.13,94.13,0,0,1,210.85,172.47Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm88-29.84q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.21,107.21,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.71,107.71,0,0,0-26.25-10.87,8,8,0,0,0-7.06,1.49L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.21,107.21,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06Zm-16.1-6.5a73.93,73.93,0,0,1,0,8.68,8,8,0,0,0,1.74,5.48l14.19,17.73a91.57,91.57,0,0,1-6.23,15L187,173.11a8,8,0,0,0-5.1,2.64,74.11,74.11,0,0,1-6.14,6.14,8,8,0,0,0-2.64,5.1l-2.51,22.58a91.32,91.32,0,0,1-15,6.23l-17.74-14.19a8,8,0,0,0-5-1.75h-.48a73.93,73.93,0,0,1-8.68,0,8,8,0,0,0-5.48,1.74L100.45,215.8a91.57,91.57,0,0,1-15-6.23L82.89,187a8,8,0,0,0-2.64-5.1,74.11,74.11,0,0,1-6.14-6.14,8,8,0,0,0-5.1-2.64L46.43,170.6a91.32,91.32,0,0,1-6.23-15l14.19-17.74a8,8,0,0,0,1.74-5.48,73.93,73.93,0,0,1,0-8.68,8,8,0,0,0-1.74-5.48L40.2,100.45a91.57,91.57,0,0,1,6.23-15L69,82.89a8,8,0,0,0,5.1-2.64,74.11,74.11,0,0,1,6.14-6.14A8,8,0,0,0,82.89,69L85.4,46.43a91.32,91.32,0,0,1,15-6.23l17.74,14.19a8,8,0,0,0,5.48,1.74,73.93,73.93,0,0,1,8.68,0,8,8,0,0,0,5.48-1.74L155.55,40.2a91.57,91.57,0,0,1,15,6.23L173.11,69a8,8,0,0,0,2.64,5.1,74.11,74.11,0,0,1,6.14,6.14,8,8,0,0,0,5.1,2.64l22.58,2.51a91.32,91.32,0,0,1,6.23,15l-14.19,17.74A8,8,0,0,0,199.87,123.66Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,84a44,44,0,1,0,44,44A44.05,44.05,0,0,0,128,84Zm0,80a36,36,0,1,1,36-36A36,36,0,0,1,128,164Zm83.93-32.49q.13-3.51,0-7l15.83-19.79a4,4,0,0,0,.75-3.53A103.64,103.64,0,0,0,218,75.9a4,4,0,0,0-3-2l-25.19-2.8c-1.58-1.71-3.24-3.37-4.95-4.95L182.07,41a4,4,0,0,0-2-3A104,104,0,0,0,154.82,27.5a4,4,0,0,0-3.53.74L131.51,44.07q-3.51-.14-7,0L104.7,28.24a4,4,0,0,0-3.53-.75A103.64,103.64,0,0,0,75.9,38a4,4,0,0,0-2,3l-2.8,25.19c-1.71,1.58-3.37,3.24-4.95,4.95L41,73.93a4,4,0,0,0-3,2A104,104,0,0,0,27.5,101.18a4,4,0,0,0,.74,3.53l15.83,19.78q-.14,3.51,0,7L28.24,151.3a4,4,0,0,0-.75,3.53A103.64,103.64,0,0,0,38,180.1a4,4,0,0,0,3,2l25.19,2.8c1.58,1.71,3.24,3.37,4.95,4.95l2.8,25.2a4,4,0,0,0,2,3,104,104,0,0,0,25.28,10.46,4,4,0,0,0,3.53-.74l19.78-15.83q3.51.13,7,0l19.79,15.83a4,4,0,0,0,2.5.88,4,4,0,0,0,1-.13A103.64,103.64,0,0,0,180.1,218a4,4,0,0,0,2-3l2.8-25.19c1.71-1.58,3.37-3.24,4.95-4.95l25.2-2.8a4,4,0,0,0,3-2,104,104,0,0,0,10.46-25.28,4,4,0,0,0-.74-3.53Zm.17,42.83-24.67,2.74a4,4,0,0,0-2.55,1.32,76.2,76.2,0,0,1-6.48,6.48,4,4,0,0,0-1.32,2.55l-2.74,24.66a95.45,95.45,0,0,1-19.64,8.15l-19.38-15.51a4,4,0,0,0-2.5-.87h-.24a73.67,73.67,0,0,1-9.16,0,4,4,0,0,0-2.74.87l-19.37,15.5a95.33,95.33,0,0,1-19.65-8.13l-2.74-24.67a4,4,0,0,0-1.32-2.55,76.2,76.2,0,0,1-6.48-6.48,4,4,0,0,0-2.55-1.32l-24.66-2.74a95.45,95.45,0,0,1-8.15-19.64l15.51-19.38a4,4,0,0,0,.87-2.74,77.76,77.76,0,0,1,0-9.16,4,4,0,0,0-.87-2.74l-15.5-19.37A95.33,95.33,0,0,1,43.9,81.66l24.67-2.74a4,4,0,0,0,2.55-1.32,76.2,76.2,0,0,1,6.48-6.48,4,4,0,0,0,1.32-2.55l2.74-24.66a95.45,95.45,0,0,1,19.64-8.15l19.38,15.51a4,4,0,0,0,2.74.87,73.67,73.67,0,0,1,9.16,0,4,4,0,0,0,2.74-.87l19.37-15.5a95.33,95.33,0,0,1,19.65,8.13l2.74,24.67a4,4,0,0,0,1.32,2.55,76.2,76.2,0,0,1,6.48,6.48,4,4,0,0,0,2.55,1.32l24.66,2.74a95.45,95.45,0,0,1,8.15,19.64l-15.51,19.38a4,4,0,0,0-.87,2.74,77.76,77.76,0,0,1,0,9.16,4,4,0,0,0,.87,2.74l15.5,19.37A95.33,95.33,0,0,1,212.1,174.34Z"}))]]),l2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M228,64a36,36,0,1,0-48,33.94V112a4,4,0,0,1-4,4H80a4,4,0,0,1-4-4V97.94a36,36,0,1,0-24,0V112a28,28,0,0,0,28,28h36v18.06a36,36,0,1,0,24,0V140h36a28,28,0,0,0,28-28V97.94A36.07,36.07,0,0,0,228,64ZM64,52A12,12,0,1,1,52,64,12,12,0,0,1,64,52Zm64,152a12,12,0,1,1,12-12A12,12,0,0,1,128,204ZM192,76a12,12,0,1,1,12-12A12,12,0,0,1,192,76Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M88,64A24,24,0,1,1,64,40,24,24,0,0,1,88,64ZM192,40a24,24,0,1,0,24,24A24,24,0,0,0,192,40Z",opacity:"0.2"}),D.createElement("path",{d:"M224,64a32,32,0,1,0-40,31v17a8,8,0,0,1-8,8H80a8,8,0,0,1-8-8V95a32,32,0,1,0-16,0v17a24,24,0,0,0,24,24h40v25a32,32,0,1,0,16,0V136h40a24,24,0,0,0,24-24V95A32.06,32.06,0,0,0,224,64ZM48,64A16,16,0,1,1,64,80,16,16,0,0,1,48,64Zm96,128a16,16,0,1,1-16-16A16,16,0,0,1,144,192ZM192,80a16,16,0,1,1,16-16A16,16,0,0,1,192,80Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,64a32,32,0,1,0-40,31v17a8,8,0,0,1-8,8H80a8,8,0,0,1-8-8V95a32,32,0,1,0-16,0v17a24,24,0,0,0,24,24h40v25a32,32,0,1,0,16,0V136h40a24,24,0,0,0,24-24V95A32.06,32.06,0,0,0,224,64ZM144,192a16,16,0,1,1-16-16A16,16,0,0,1,144,192Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M222,64a30,30,0,1,0-36,29.4V112a10,10,0,0,1-10,10H80a10,10,0,0,1-10-10V93.4a30,30,0,1,0-12,0V112a22,22,0,0,0,22,22h42v28.6a30,30,0,1,0,12,0V134h42a22,22,0,0,0,22-22V93.4A30.05,30.05,0,0,0,222,64ZM46,64A18,18,0,1,1,64,82,18,18,0,0,1,46,64ZM146,192a18,18,0,1,1-18-18A18,18,0,0,1,146,192ZM192,82a18,18,0,1,1,18-18A18,18,0,0,1,192,82Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,64a32,32,0,1,0-40,31v17a8,8,0,0,1-8,8H80a8,8,0,0,1-8-8V95a32,32,0,1,0-16,0v17a24,24,0,0,0,24,24h40v25a32,32,0,1,0,16,0V136h40a24,24,0,0,0,24-24V95A32.06,32.06,0,0,0,224,64ZM48,64A16,16,0,1,1,64,80,16,16,0,0,1,48,64Zm96,128a16,16,0,1,1-16-16A16,16,0,0,1,144,192ZM192,80a16,16,0,1,1,16-16A16,16,0,0,1,192,80Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M220,64a28,28,0,1,0-32,27.71V112a12,12,0,0,1-12,12H80a12,12,0,0,1-12-12V91.71a28,28,0,1,0-8,0V112a20,20,0,0,0,20,20h44v32.29a28,28,0,1,0,8,0V132h44a20,20,0,0,0,20-20V91.71A28,28,0,0,0,220,64ZM44,64A20,20,0,1,1,64,84,20,20,0,0,1,44,64ZM148,192a20,20,0,1,1-20-20A20,20,0,0,1,148,192ZM192,84a20,20,0,1,1,20-20A20,20,0,0,1,192,84Z"}))]]),c2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,187a113.4,113.4,0,0,1-20.39-35h40.82a116.94,116.94,0,0,1-10,20.77A108.61,108.61,0,0,1,128,207Zm-26.49-59a135.42,135.42,0,0,1,0-40h53a135.42,135.42,0,0,1,0,40ZM44,128a83.49,83.49,0,0,1,2.43-20H77.25a160.63,160.63,0,0,0,0,40H46.43A83.49,83.49,0,0,1,44,128Zm84-79a113.4,113.4,0,0,1,20.39,35H107.59a116.94,116.94,0,0,1,10-20.77A108.61,108.61,0,0,1,128,49Zm50.73,59h30.82a83.52,83.52,0,0,1,0,40H178.75a160.63,160.63,0,0,0,0-40Zm20.77-24H173.71a140.82,140.82,0,0,0-15.5-34.36A84.51,84.51,0,0,1,199.52,84ZM97.79,49.64A140.82,140.82,0,0,0,82.29,84H56.48A84.51,84.51,0,0,1,97.79,49.64ZM56.48,172H82.29a140.82,140.82,0,0,0,15.5,34.36A84.51,84.51,0,0,1,56.48,172Zm101.73,34.36A140.82,140.82,0,0,0,173.71,172h25.81A84.51,84.51,0,0,1,158.21,206.36Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z",opacity:"0.2"}),D.createElement("path",{d:"M128,24h0A104,104,0,1,0,232,128,104.12,104.12,0,0,0,128,24Zm88,104a87.61,87.61,0,0,1-3.33,24H174.16a157.44,157.44,0,0,0,0-48h38.51A87.61,87.61,0,0,1,216,128ZM102,168H154a115.11,115.11,0,0,1-26,45A115.27,115.27,0,0,1,102,168Zm-3.9-16a140.84,140.84,0,0,1,0-48h59.88a140.84,140.84,0,0,1,0,48ZM40,128a87.61,87.61,0,0,1,3.33-24H81.84a157.44,157.44,0,0,0,0,48H43.33A87.61,87.61,0,0,1,40,128ZM154,88H102a115.11,115.11,0,0,1,26-45A115.27,115.27,0,0,1,154,88Zm52.33,0H170.71a135.28,135.28,0,0,0-22.3-45.6A88.29,88.29,0,0,1,206.37,88ZM107.59,42.4A135.28,135.28,0,0,0,85.29,88H49.63A88.29,88.29,0,0,1,107.59,42.4ZM49.63,168H85.29a135.28,135.28,0,0,0,22.3,45.6A88.29,88.29,0,0,1,49.63,168Zm98.78,45.6a135.28,135.28,0,0,0,22.3-45.6h35.66A88.29,88.29,0,0,1,148.41,213.6Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24h0A104,104,0,1,0,232,128,104.12,104.12,0,0,0,128,24Zm78.36,64H170.71a135.28,135.28,0,0,0-22.3-45.6A88.29,88.29,0,0,1,206.37,88ZM216,128a87.61,87.61,0,0,1-3.33,24H174.16a157.44,157.44,0,0,0,0-48h38.51A87.61,87.61,0,0,1,216,128ZM128,43a115.27,115.27,0,0,1,26,45H102A115.11,115.11,0,0,1,128,43ZM102,168H154a115.11,115.11,0,0,1-26,45A115.27,115.27,0,0,1,102,168Zm-3.9-16a140.84,140.84,0,0,1,0-48h59.88a140.84,140.84,0,0,1,0,48Zm50.35,61.6a135.28,135.28,0,0,0,22.3-45.6h35.66A88.29,88.29,0,0,1,148.41,213.6Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,26A102,102,0,1,0,230,128,102.12,102.12,0,0,0,128,26Zm81.57,64H169.19a132.58,132.58,0,0,0-25.73-50.67A90.29,90.29,0,0,1,209.57,90ZM218,128a89.7,89.7,0,0,1-3.83,26H171.81a155.43,155.43,0,0,0,0-52h42.36A89.7,89.7,0,0,1,218,128Zm-90,87.83a110,110,0,0,1-15.19-19.45A124.24,124.24,0,0,1,99.35,166h57.3a124.24,124.24,0,0,1-13.46,30.38A110,110,0,0,1,128,215.83ZM96.45,154a139.18,139.18,0,0,1,0-52h63.1a139.18,139.18,0,0,1,0,52ZM38,128a89.7,89.7,0,0,1,3.83-26H84.19a155.43,155.43,0,0,0,0,52H41.83A89.7,89.7,0,0,1,38,128Zm90-87.83a110,110,0,0,1,15.19,19.45A124.24,124.24,0,0,1,156.65,90H99.35a124.24,124.24,0,0,1,13.46-30.38A110,110,0,0,1,128,40.17Zm-15.46-.84A132.58,132.58,0,0,0,86.81,90H46.43A90.29,90.29,0,0,1,112.54,39.33ZM46.43,166H86.81a132.58,132.58,0,0,0,25.73,50.67A90.29,90.29,0,0,1,46.43,166Zm97,50.67A132.58,132.58,0,0,0,169.19,166h40.38A90.29,90.29,0,0,1,143.46,216.67Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24h0A104,104,0,1,0,232,128,104.12,104.12,0,0,0,128,24Zm88,104a87.61,87.61,0,0,1-3.33,24H174.16a157.44,157.44,0,0,0,0-48h38.51A87.61,87.61,0,0,1,216,128ZM102,168H154a115.11,115.11,0,0,1-26,45A115.27,115.27,0,0,1,102,168Zm-3.9-16a140.84,140.84,0,0,1,0-48h59.88a140.84,140.84,0,0,1,0,48ZM40,128a87.61,87.61,0,0,1,3.33-24H81.84a157.44,157.44,0,0,0,0,48H43.33A87.61,87.61,0,0,1,40,128ZM154,88H102a115.11,115.11,0,0,1,26-45A115.27,115.27,0,0,1,154,88Zm52.33,0H170.71a135.28,135.28,0,0,0-22.3-45.6A88.29,88.29,0,0,1,206.37,88ZM107.59,42.4A135.28,135.28,0,0,0,85.29,88H49.63A88.29,88.29,0,0,1,107.59,42.4ZM49.63,168H85.29a135.28,135.28,0,0,0,22.3,45.6A88.29,88.29,0,0,1,49.63,168Zm98.78,45.6a135.28,135.28,0,0,0,22.3-45.6h35.66A88.29,88.29,0,0,1,148.41,213.6Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,28h0A100,100,0,1,0,228,128,100.11,100.11,0,0,0,128,28Zm0,190.61c-6.33-6.09-23-24.41-31.27-54.61h62.54C151,194.2,134.33,212.52,128,218.61ZM94.82,156a140.42,140.42,0,0,1,0-56h66.36a140.42,140.42,0,0,1,0,56ZM128,37.39c6.33,6.09,23,24.41,31.27,54.61H96.73C105,61.8,121.67,43.48,128,37.39ZM169.41,100h46.23a92.09,92.09,0,0,1,0,56H169.41a152.65,152.65,0,0,0,0-56Zm43.25-8h-45a129.39,129.39,0,0,0-29.19-55.4A92.25,92.25,0,0,1,212.66,92ZM117.54,36.6A129.39,129.39,0,0,0,88.35,92h-45A92.25,92.25,0,0,1,117.54,36.6ZM40.36,100H86.59a152.65,152.65,0,0,0,0,56H40.36a92.09,92.09,0,0,1,0-56Zm3,64h45a129.39,129.39,0,0,0,29.19,55.4A92.25,92.25,0,0,1,43.34,164Zm95.12,55.4A129.39,129.39,0,0,0,167.65,164h45A92.25,92.25,0,0,1,138.46,219.4Z"}))]]),u2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,152a35.77,35.77,0,0,0-16.46,4l-21.39-16.64A35.49,35.49,0,0,0,164,128.65l10.35-3.44A36,36,0,1,0,164,100c0,1.11.06,2.21.16,3.3l-7.78,2.59A36,36,0,0,0,128,92c-1,0-1.88,0-2.81.12l-4.45-10A36,36,0,1,0,96,92c1,0,1.88,0,2.81-.12l4.45,10a35.91,35.91,0,0,0-8.59,39.7L73.39,160.49a36,36,0,1,0,15.94,17.93l21.28-18.91a35.91,35.91,0,0,0,36.8-1.21L167,173.56A36,36,0,1,0,200,152Zm0-64a12,12,0,1,1-12,12A12,12,0,0,1,200,88ZM84,56A12,12,0,1,1,96,68,12,12,0,0,1,84,56ZM56,204a12,12,0,1,1,12-12A12,12,0,0,1,56,204Zm60-76a12,12,0,1,1,12,12A12,12,0,0,1,116,128Zm84,72a12,12,0,1,1,12-12A12,12,0,0,1,200,200Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M152,128a24,24,0,1,1-24-24A24,24,0,0,1,152,128Z",opacity:"0.2"}),D.createElement("path",{d:"M200,152a31.84,31.84,0,0,0-19.53,6.68l-23.11-18A31.65,31.65,0,0,0,160,128c0-.74,0-1.48-.08-2.21l13.23-4.41A32,32,0,1,0,168,104c0,.74,0,1.48.08,2.21l-13.23,4.41A32,32,0,0,0,128,96a32.59,32.59,0,0,0-5.27.44L115.89,81A32,32,0,1,0,96,88a32.59,32.59,0,0,0,5.27-.44l6.84,15.4a31.92,31.92,0,0,0-8.57,39.64L73.83,165.44a32.06,32.06,0,1,0,10.63,12l25.71-22.84a31.91,31.91,0,0,0,37.36-1.24l23.11,18A31.65,31.65,0,0,0,168,184a32,32,0,1,0,32-32Zm0-64a16,16,0,1,1-16,16A16,16,0,0,1,200,88ZM80,56A16,16,0,1,1,96,72,16,16,0,0,1,80,56ZM56,208a16,16,0,1,1,16-16A16,16,0,0,1,56,208Zm56-80a16,16,0,1,1,16,16A16,16,0,0,1,112,128Zm88,72a16,16,0,1,1,16-16A16,16,0,0,1,200,200Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,152a31.84,31.84,0,0,0-19.53,6.68l-23.11-18A31.65,31.65,0,0,0,160,128c0-.74,0-1.48-.08-2.21l13.23-4.41A32,32,0,1,0,168,104c0,.74,0,1.48.08,2.21l-13.23,4.41A32,32,0,0,0,128,96a32.59,32.59,0,0,0-5.27.44L115.89,81A32,32,0,1,0,96,88a32.59,32.59,0,0,0,5.27-.44l6.84,15.4a31.92,31.92,0,0,0-8.57,39.64L73.83,165.44a32.06,32.06,0,1,0,10.63,12l25.71-22.84a31.91,31.91,0,0,0,37.36-1.24l23.11,18A31.65,31.65,0,0,0,168,184a32,32,0,1,0,32-32Zm0-64a16,16,0,1,1-16,16A16,16,0,0,1,200,88ZM80,56A16,16,0,1,1,96,72,16,16,0,0,1,80,56ZM56,208a16,16,0,1,1,16-16A16,16,0,0,1,56,208Zm144-8a16,16,0,1,1,16-16A16,16,0,0,1,200,200Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,154a29.87,29.87,0,0,0-19.5,7.23L154.88,141.3A29.83,29.83,0,0,0,158,128a30.52,30.52,0,0,0-.22-3.6L174,119a30,30,0,1,0-4-15,30.52,30.52,0,0,0,.22,3.6L154,113a29.91,29.91,0,0,0-32.42-14.31l-8.14-18.3a30,30,0,1,0-11,4.88l8.14,18.3A29.92,29.92,0,0,0,102.06,143L74,168a30.08,30.08,0,1,0,8,9L110,152a29.91,29.91,0,0,0,37.47-1.23l25.62,19.93A30,30,0,1,0,200,154Zm0-68a18,18,0,1,1-18,18A18,18,0,0,1,200,86ZM78,56A18,18,0,1,1,96,74,18,18,0,0,1,78,56ZM56,210a18,18,0,1,1,18-18A18,18,0,0,1,56,210Zm72-64a18,18,0,1,1,18-18A18,18,0,0,1,128,146Zm72,56a18,18,0,1,1,18-18A18,18,0,0,1,200,202Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,152a31.84,31.84,0,0,0-19.53,6.68l-23.11-18A31.65,31.65,0,0,0,160,128c0-.74,0-1.48-.08-2.21l13.23-4.41A32,32,0,1,0,168,104c0,.74,0,1.48.08,2.21l-13.23,4.41A32,32,0,0,0,128,96a32.59,32.59,0,0,0-5.27.44L115.89,81A32,32,0,1,0,96,88a32.59,32.59,0,0,0,5.27-.44l6.84,15.4a31.92,31.92,0,0,0-8.57,39.64L73.83,165.44a32.06,32.06,0,1,0,10.63,12l25.71-22.84a31.91,31.91,0,0,0,37.36-1.24l23.11,18A31.65,31.65,0,0,0,168,184a32,32,0,1,0,32-32Zm0-64a16,16,0,1,1-16,16A16,16,0,0,1,200,88ZM80,56A16,16,0,1,1,96,72,16,16,0,0,1,80,56ZM56,208a16,16,0,1,1,16-16A16,16,0,0,1,56,208Zm56-80a16,16,0,1,1,16,16A16,16,0,0,1,112,128Zm88,72a16,16,0,1,1,16-16A16,16,0,0,1,200,200Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,156a27.87,27.87,0,0,0-19.4,7.84l-28.28-22A27.78,27.78,0,0,0,156,128a28.09,28.09,0,0,0-.45-5L175,116.55a28.07,28.07,0,1,0-2.53-7.58L153,115.45A28,28,0,0,0,128,100a27.68,27.68,0,0,0-7.6,1.06l-9.5-21.37A28,28,0,1,0,96,84a27.68,27.68,0,0,0,7.6-1.06l9.5,21.37a27.95,27.95,0,0,0-8.46,39.1L74,170.61a28,28,0,1,0,5.32,6l30.6-27.2a27.92,27.92,0,0,0,37.44-1.23l28.28,22A28,28,0,1,0,200,156Zm0-72a20,20,0,1,1-20,20A20,20,0,0,1,200,84ZM76,56A20,20,0,1,1,96,76,20,20,0,0,1,76,56ZM56,212a20,20,0,1,1,20-20A20,20,0,0,1,56,212Zm72-64a20,20,0,1,1,20-20A20,20,0,0,1,128,148Zm72,56a20,20,0,1,1,20-20A20,20,0,0,1,200,204Z"}))]]),d2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,36H56A20,20,0,0,0,36,56V200a20,20,0,0,0,20,20H200a20,20,0,0,0,20-20V56A20,20,0,0,0,200,36Zm-4,80H140V60h56ZM116,60v56H60V60ZM60,140h56v56H60Zm80,56V140h56v56Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,56V200a8,8,0,0,1-8,8H56a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8H200A8,8,0,0,1,208,56Z",opacity:"0.2"}),D.createElement("path",{d:"M200,40H56A16,16,0,0,0,40,56V200a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,80H136V56h64ZM120,56v64H56V56ZM56,136h64v64H56Zm144,64H136V136h64v64Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,56v60a4,4,0,0,1-4,4H136V44a4,4,0,0,1,4-4h60A16,16,0,0,1,216,56ZM116,40H56A16,16,0,0,0,40,56v60a4,4,0,0,0,4,4h76V44A4,4,0,0,0,116,40Zm96,96H136v76a4,4,0,0,0,4,4h60a16,16,0,0,0,16-16V140A4,4,0,0,0,212,136ZM40,140v60a16,16,0,0,0,16,16h60a4,4,0,0,0,4-4V136H44A4,4,0,0,0,40,140Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,42H56A14,14,0,0,0,42,56V200a14,14,0,0,0,14,14H200a14,14,0,0,0,14-14V56A14,14,0,0,0,200,42Zm2,14v66H134V54h66A2,2,0,0,1,202,56ZM56,54h66v68H54V56A2,2,0,0,1,56,54ZM54,200V134h68v68H56A2,2,0,0,1,54,200Zm146,2H134V134h68v66A2,2,0,0,1,200,202Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,40H56A16,16,0,0,0,40,56V200a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,80H136V56h64ZM120,56v64H56V56ZM56,136h64v64H56Zm144,64H136V136h64v64Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,44H56A12,12,0,0,0,44,56V200a12,12,0,0,0,12,12H200a12,12,0,0,0,12-12V56A12,12,0,0,0,200,44Zm4,12v68H132V52h68A4,4,0,0,1,204,56ZM56,52h68v72H52V56A4,4,0,0,1,56,52ZM52,200V132h72v72H56A4,4,0,0,1,52,200Zm148,4H132V132h72v68A4,4,0,0,1,200,204Z"}))]]),h2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M253.88,108.11l-25.53-51a20,20,0,0,0-26.83-9L178.34,59.7,131.7,44.58a12.14,12.14,0,0,0-7.4,0L77.66,59.7,54.48,48.11a20,20,0,0,0-26.83,9L2.12,108.11a20,20,0,0,0,9,26.83l26.67,13.34,51.18,37.41A12.15,12.15,0,0,0,93,187.62l62,16a12.27,12.27,0,0,0,3,.38,12,12,0,0,0,8.48-3.52l52.62-52.62,25.83-12.92a20,20,0,0,0,8.95-26.83Zm-58.12,29.15-27.52-26a12,12,0,0,0-16.76.26c-9.66,9.74-25.06,16.81-40.81,9.55l38.19-37h22.72l25.81,51.63ZM47.32,71.37,60.59,78l-22,43.9-13.27-6.63Zm107,107.3L101.23,165l-42-30.66L85.17,82.5,128,68.61l1.69.55L90,107.68l-.13.12a20,20,0,0,0,3.4,31c20.95,13.39,46,12.07,66.33-2.73l19.2,18.15Zm63-56.77-22-43.9,13.27-6.63,21.95,43.9ZM118.55,219a12,12,0,0,1-14.62,8.62l-26.6-6.87a12,12,0,0,1-4.08-1.93L48.92,201a12,12,0,0,1,14.16-19.37l22.47,16.42,24.38,6.29A12,12,0,0,1,118.55,219Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,152l-40,40L96,176,40,136,72.68,70.63,128,56l55.32,14.63L183.6,72H144L98.34,116.29a8,8,0,0,0,1.38,12.42C117.23,139.9,141,139.13,160,120Z",opacity:"0.2"}),D.createElement("path",{d:"M254.3,107.91,228.78,56.85a16,16,0,0,0-21.47-7.15L182.44,62.13,130.05,48.27a8.14,8.14,0,0,0-4.1,0L73.56,62.13,48.69,49.7a16,16,0,0,0-21.47,7.15L1.7,107.9a16,16,0,0,0,7.15,21.47l27,13.51,55.49,39.63a8.06,8.06,0,0,0,2.71,1.25l64,16a8,8,0,0,0,7.6-2.1l55.07-55.08,26.42-13.21a16,16,0,0,0,7.15-21.46Zm-54.89,33.37L165,113.72a8,8,0,0,0-10.68.61C136.51,132.27,116.66,130,104,122L147.24,80h31.81l27.21,54.41ZM41.53,64,62,74.22,36.43,125.27,16,115.06Zm116,119.13L99.42,168.61l-49.2-35.14,28-56L128,64.28l9.8,2.59-45,43.68-.08.09a16,16,0,0,0,2.72,24.81c20.56,13.13,45.37,11,64.91-5L188,152.66Zm62-57.87-25.52-51L214.47,64,240,115.06Zm-87.75,92.67a8,8,0,0,1-7.75,6.06,8.13,8.13,0,0,1-1.95-.24L80.41,213.33a7.89,7.89,0,0,1-2.71-1.25L51.35,193.26a8,8,0,0,1,9.3-13l25.11,17.94L126,208.24A8,8,0,0,1,131.82,217.94Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M254.3,107.91,228.78,56.85a16,16,0,0,0-21.47-7.15L182.44,62.13,130.05,48.27a8.14,8.14,0,0,0-4.1,0L73.56,62.13,48.69,49.7a16,16,0,0,0-21.47,7.15L1.7,107.9a16,16,0,0,0,7.15,21.47l27,13.51,55.49,39.63a8.06,8.06,0,0,0,2.71,1.25l64,16a8,8,0,0,0,7.6-2.1l40-40,15.08-15.08,26.42-13.21a16,16,0,0,0,7.15-21.46Zm-54.89,33.37L165,113.72a8,8,0,0,0-10.68.61C136.51,132.27,116.66,130,104,122L147.24,80h31.81l27.21,54.41Zm-41.87,41.86L99.42,168.61l-49.2-35.14,28-56L128,64.28l9.8,2.59-45,43.68-.08.09a16,16,0,0,0,2.72,24.81c20.56,13.13,45.37,11,64.91-5L188,152.66Zm-25.72,34.8a8,8,0,0,1-7.75,6.06,8.13,8.13,0,0,1-1.95-.24L80.41,213.33a7.89,7.89,0,0,1-2.71-1.25L51.35,193.26a8,8,0,0,1,9.3-13l25.11,17.94L126,208.24A8,8,0,0,1,131.82,217.94Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M252.51,108.8,227,57.75a14,14,0,0,0-18.78-6.27L182.66,64.26,129.53,50.2a6.1,6.1,0,0,0-3.06,0L73.34,64.26,47.79,51.48A14,14,0,0,0,29,57.75L3.49,108.8a14,14,0,0,0,6.26,18.78L36.9,141.16l55.61,39.72a6,6,0,0,0,2,.94l64,16A6.08,6.08,0,0,0,160,198a6,6,0,0,0,4.24-1.76l55.31-55.31,26.7-13.35a14,14,0,0,0,6.26-18.78Zm-53,35.16-35.8-28.68a6,6,0,0,0-8,.45c-18.65,18.79-39.5,16.42-52.79,7.92a2,2,0,0,1-.94-1.5,1.9,1.9,0,0,1,.51-1.55L146.43,78h33.86l28.41,56.82ZM14.11,115.69a2,2,0,0,1,.11-1.52L39.74,63.11a2,2,0,0,1,1.8-1.1,2,2,0,0,1,.89.21l22.21,11.1L37.32,128l-22.21-11.1A2,2,0,0,1,14.11,115.69Zm144.05,69.67-59.6-14.9L47.66,134.1,76.84,75.75,128,62.21l14.8,3.92a5.92,5.92,0,0,0-3,1.57L94.1,112.05a14,14,0,0,0,2.39,21.72c20.22,12.92,44.75,10.49,63.8-5.89L191,152.5Zm83.73-69.67a2,2,0,0,1-1,1.16L218.68,128,191.36,73.32l22.21-11.1a2,2,0,0,1,1.53-.11,2,2,0,0,1,1.16,1l25.52,51.06A2,2,0,0,1,241.89,115.69Zm-112,101.76a6,6,0,0,1-7.27,4.37L80.89,211.39a5.88,5.88,0,0,1-2-.94L52.52,191.64a6,6,0,1,1,7-9.77L84.91,200l40.61,10.15A6,6,0,0,1,129.88,217.45Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M254.3,107.91,228.78,56.85a16,16,0,0,0-21.47-7.15L182.44,62.13,130.05,48.27a8.14,8.14,0,0,0-4.1,0L73.56,62.13,48.69,49.7a16,16,0,0,0-21.47,7.15L1.7,107.9a16,16,0,0,0,7.15,21.47l27,13.51,55.49,39.63a8.06,8.06,0,0,0,2.71,1.25l64,16a8,8,0,0,0,7.6-2.1l55.07-55.08,26.42-13.21a16,16,0,0,0,7.15-21.46Zm-54.89,33.37L165,113.72a8,8,0,0,0-10.68.61C136.51,132.27,116.66,130,104,122L147.24,80h31.81l27.21,54.41ZM41.53,64,62,74.22,36.43,125.27,16,115.06Zm116,119.13L99.42,168.61l-49.2-35.14,28-56L128,64.28l9.8,2.59-45,43.68-.08.09a16,16,0,0,0,2.72,24.81c20.56,13.13,45.37,11,64.91-5L188,152.66Zm62-57.87-25.52-51L214.47,64,240,115.06Zm-87.75,92.67a8,8,0,0,1-7.75,6.06,8.13,8.13,0,0,1-1.95-.24L80.41,213.33a7.89,7.89,0,0,1-2.71-1.25L51.35,193.26a8,8,0,0,1,9.3-13l25.11,17.94L126,208.24A8,8,0,0,1,131.82,217.94Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M250.73,109.69l-25.53-51a12,12,0,0,0-16.1-5.37L182.88,66.38,129,52.14a3.92,3.92,0,0,0-2,0L73.12,66.38,46.9,53.27a12,12,0,0,0-16.1,5.37L5.27,109.69a12,12,0,0,0,5.37,16.1l27.29,13.65,55.75,39.82a3.87,3.87,0,0,0,1.35.62l64,16a4,4,0,0,0,3.8-1l55.54-55.54,27-13.5a12,12,0,0,0,5.37-16.1Zm-51,36.95-37.2-29.8a4,4,0,0,0-5.34.3c-19.49,19.64-41.34,17.11-55.29,8.2a4.07,4.07,0,0,1-1.85-3,3.91,3.91,0,0,1,1.11-3.21L145.62,76h35.91l29.6,59.21ZM12.21,116.32a4,4,0,0,1,.22-3L38,62.22h0A4,4,0,0,1,41.54,60a4,4,0,0,1,1.78.43l24,12L38.21,130.64l-24-12A4,4,0,0,1,12.21,116.32Zm146.56,71.25L97.71,172.3l-52.6-37.57L75.45,74,128,60.14,157.72,68H144a4,4,0,0,0-2.79,1.13l-45.7,44.33a12,12,0,0,0,2.06,18.62c19.88,12.71,44.13,10,62.66-6.81L194,152.33Zm85-71.25a4,4,0,0,1-2,2.32l-24,12L188.68,72.43l24-12A4,4,0,0,1,218,62.22l25.53,51.05A4,4,0,0,1,243.79,116.32ZM127.94,217a4,4,0,0,1-3.88,3,4.09,4.09,0,0,1-1-.12L81.38,209.45a4,4,0,0,1-1.36-.62L53.68,190a4,4,0,0,1,4.65-6.51l25.72,18.37,41,10.25A4,4,0,0,1,127.94,217Z"}))]]),f2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M204,75.64V40a20,20,0,0,0-20-20H72A20,20,0,0,0,52,40V76a20.1,20.1,0,0,0,8,16l48,36L60,164a20.1,20.1,0,0,0-8,16v36a20,20,0,0,0,20,20H184a20,20,0,0,0,20-20V180.36a20.13,20.13,0,0,0-7.94-16L147.9,128l48.16-36.4A20.13,20.13,0,0,0,204,75.64ZM180,212H76V182l52-39,52,39.33Zm0-138.35L128,113,76,74V44H180Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M188.82,82,128,128,67.2,82.4A8,8,0,0,1,64,76V40a8,8,0,0,1,8-8H184a8,8,0,0,1,8,8V75.64A8,8,0,0,1,188.82,82ZM64,180v36a8,8,0,0,0,8,8H184a8,8,0,0,0,8-8V180.36a8,8,0,0,0-3.18-6.38L128,128,67.2,173.6A8,8,0,0,0,64,180Z",opacity:"0.2"}),D.createElement("path",{d:"M200,75.64V40a16,16,0,0,0-16-16H72A16,16,0,0,0,56,40V76a16.07,16.07,0,0,0,6.4,12.8L114.67,128,62.4,167.2A16.07,16.07,0,0,0,56,180v36a16,16,0,0,0,16,16H184a16,16,0,0,0,16-16V180.36a16.09,16.09,0,0,0-6.35-12.77L141.27,128l52.38-39.59A16.09,16.09,0,0,0,200,75.64ZM184,216H72V180l56-42,56,42.35Zm0-140.36L128,118,72,76V40H184Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,75.64V40a16,16,0,0,0-16-16H72A16,16,0,0,0,56,40V76a16.08,16.08,0,0,0,6.41,12.8L114.67,128,62.4,167.2A16.07,16.07,0,0,0,56,180v36a16,16,0,0,0,16,16H184a16,16,0,0,0,16-16V180.36a16,16,0,0,0-6.36-12.77L141.26,128l52.38-39.59A16.05,16.05,0,0,0,200,75.64Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M198,75.64V40a14,14,0,0,0-14-14H72A14,14,0,0,0,58,40V76a14.06,14.06,0,0,0,5.6,11.2L118,128,63.6,168.8A14.06,14.06,0,0,0,58,180v36a14,14,0,0,0,14,14H184a14,14,0,0,0,14-14V180.36a14.08,14.08,0,0,0-5.56-11.17L138,128l54.49-41.19A14.08,14.08,0,0,0,198,75.64ZM186,180.36V216a2,2,0,0,1-2,2H72a2,2,0,0,1-2-2V180a2,2,0,0,1,.8-1.6L128,135.51l57.22,43.25A2,2,0,0,1,186,180.36Zm0-104.72a2,2,0,0,1-.79,1.6L128,120.49,70.8,77.6A2,2,0,0,1,70,76V40a2,2,0,0,1,2-2H184a2,2,0,0,1,2,2Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,75.64V40a16,16,0,0,0-16-16H72A16,16,0,0,0,56,40V76a16.07,16.07,0,0,0,6.4,12.8L114.67,128,62.4,167.2A16.07,16.07,0,0,0,56,180v36a16,16,0,0,0,16,16H184a16,16,0,0,0,16-16V180.36a16.09,16.09,0,0,0-6.35-12.77L141.27,128l52.38-39.6A16.05,16.05,0,0,0,200,75.64ZM184,216H72V180l56-42,56,42.35Zm0-140.36L128,118,72,76V40H184Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M196,75.64V40a12,12,0,0,0-12-12H72A12,12,0,0,0,60,40V76a12,12,0,0,0,4.8,9.6L121.33,128,64.8,170.4A12,12,0,0,0,60,180v36a12,12,0,0,0,12,12H184a12,12,0,0,0,12-12V180.36a12.05,12.05,0,0,0-4.76-9.57L134.63,128l56.61-42.79A12.05,12.05,0,0,0,196,75.64Zm-8,104.72V216a4,4,0,0,1-4,4H72a4,4,0,0,1-4-4V180a4,4,0,0,1,1.6-3.2L128,133l58.42,44.16A4,4,0,0,1,188,180.36Zm0-104.72a4,4,0,0,1-1.59,3.19L128,123,69.6,79.2A4,4,0,0,1,68,76V40a4,4,0,0,1,4-4H184a4,4,0,0,1,4,4Z"}))]]),p2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M196,76a16,16,0,1,1-16-16A16,16,0,0,1,196,76Zm48,22.74A84.3,84.3,0,0,1,160.11,180H160a83.52,83.52,0,0,1-23.65-3.38l-7.86,7.87A12,12,0,0,1,120,188H108v12a12,12,0,0,1-12,12H84v12a12,12,0,0,1-12,12H40a20,20,0,0,1-20-20V187.31a19.86,19.86,0,0,1,5.86-14.14l53.52-53.52A84,84,0,1,1,244,98.74ZM202.43,53.57A59.48,59.48,0,0,0,158,36c-32,1-58,27.89-58,59.89a59.69,59.69,0,0,0,4.2,22.19,12,12,0,0,1-2.55,13.21L44,189v23H60V200a12,12,0,0,1,12-12H84V176a12,12,0,0,1,12-12h19l9.65-9.65a12,12,0,0,1,13.22-2.55A59.58,59.58,0,0,0,160,156h.08c32,0,58.87-26.07,59.89-58A59.55,59.55,0,0,0,202.43,53.57Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232,98.36C230.73,136.92,198.67,168,160.09,168a71.68,71.68,0,0,1-26.92-5.17h0L120,176H96v24H72v24H40a8,8,0,0,1-8-8V187.31a8,8,0,0,1,2.34-5.65l58.83-58.83h0A71.68,71.68,0,0,1,88,95.91c0-38.58,31.08-70.64,69.64-71.87A72,72,0,0,1,232,98.36Z",opacity:"0.2"}),D.createElement("path",{d:"M216.57,39.43A80,80,0,0,0,83.91,120.78L28.69,176A15.86,15.86,0,0,0,24,187.31V216a16,16,0,0,0,16,16H72a8,8,0,0,0,8-8V208H96a8,8,0,0,0,8-8V184h16a8,8,0,0,0,5.66-2.34l9.56-9.57A79.73,79.73,0,0,0,160,176h.1A80,80,0,0,0,216.57,39.43ZM224,98.1c-1.09,34.09-29.75,61.86-63.89,61.9H160a63.7,63.7,0,0,1-23.65-4.51,8,8,0,0,0-8.84,1.68L116.69,168H96a8,8,0,0,0-8,8v16H72a8,8,0,0,0-8,8v16H40V187.31l58.83-58.82a8,8,0,0,0,1.68-8.84A63.72,63.72,0,0,1,96,95.92c0-34.14,27.81-62.8,61.9-63.89A64,64,0,0,1,224,98.1ZM192,76a12,12,0,1,1-12-12A12,12,0,0,1,192,76Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216.57,39.43A80,80,0,0,0,83.91,120.78L28.69,176A15.86,15.86,0,0,0,24,187.31V216a16,16,0,0,0,16,16H72a8,8,0,0,0,8-8V208H96a8,8,0,0,0,8-8V184h16a8,8,0,0,0,5.66-2.34l9.56-9.57A79.73,79.73,0,0,0,160,176h.1A80,80,0,0,0,216.57,39.43ZM180,92a16,16,0,1,1,16-16A16,16,0,0,1,180,92Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M215.15,40.85A78,78,0,0,0,86.2,121.31l-56.1,56.1a13.94,13.94,0,0,0-4.1,9.9V216a14,14,0,0,0,14,14H72a6,6,0,0,0,6-6V206H96a6,6,0,0,0,6-6V182h18a6,6,0,0,0,4.24-1.76l10.45-10.44A77.59,77.59,0,0,0,160,174h.1A78,78,0,0,0,215.15,40.85ZM226,98.16c-1.12,35.16-30.67,63.8-65.88,63.84a65.93,65.93,0,0,1-24.51-4.67,6,6,0,0,0-6.64,1.26L117.51,170H96a6,6,0,0,0-6,6v18H72a6,6,0,0,0-6,6v18H40a2,2,0,0,1-2-2V187.31a2,2,0,0,1,.58-1.41l58.83-58.83a6,6,0,0,0,1.26-6.64A65.61,65.61,0,0,1,94,95.92C94,60.71,122.68,31.16,157.83,30A66,66,0,0,1,226,98.16ZM190,76a10,10,0,1,1-10-10A10,10,0,0,1,190,76Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216.57,39.43A80,80,0,0,0,83.91,120.78L28.69,176A15.86,15.86,0,0,0,24,187.31V216a16,16,0,0,0,16,16H72a8,8,0,0,0,8-8V208H96a8,8,0,0,0,8-8V184h16a8,8,0,0,0,5.66-2.34l9.56-9.57A79.73,79.73,0,0,0,160,176h.1A80,80,0,0,0,216.57,39.43ZM224,98.1c-1.09,34.09-29.75,61.86-63.89,61.9H160a63.7,63.7,0,0,1-23.65-4.51,8,8,0,0,0-8.84,1.68L116.69,168H96a8,8,0,0,0-8,8v16H72a8,8,0,0,0-8,8v16H40V187.31l58.83-58.82a8,8,0,0,0,1.68-8.84A63.72,63.72,0,0,1,96,95.92c0-34.14,27.81-62.8,61.9-63.89A64,64,0,0,1,224,98.1ZM192,76a12,12,0,1,1-12-12A12,12,0,0,1,192,76Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M213.74,42.26A76,76,0,0,0,88.51,121.84l-57,57A11.93,11.93,0,0,0,28,187.31V216a12,12,0,0,0,12,12H72a4,4,0,0,0,4-4V204H96a4,4,0,0,0,4-4V180h20a4,4,0,0,0,2.83-1.17l11.33-11.34A75.72,75.72,0,0,0,160,172h.1A76,76,0,0,0,213.74,42.26Zm14.22,56c-1.15,36.22-31.6,65.72-67.87,65.77H160a67.52,67.52,0,0,1-25.21-4.83,4,4,0,0,0-4.45.83l-12,12H96a4,4,0,0,0-4,4v20H72a4,4,0,0,0-4,4v20H40a4,4,0,0,1-4-4V187.31a4.06,4.06,0,0,1,1.17-2.83L96,125.66a4,4,0,0,0,.83-4.45A67.51,67.51,0,0,1,92,95.91C92,59.64,121.55,29.19,157.77,28A68,68,0,0,1,228,98.23ZM188,76a8,8,0,1,1-8-8A8,8,0,0,1,188,76Z"}))]]),g2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M117.18,188.74a12,12,0,0,1,0,17l-5.12,5.12A58.26,58.26,0,0,1,70.6,228h0A58.62,58.62,0,0,1,29.14,127.92L63.89,93.17a58.64,58.64,0,0,1,98.56,28.11,12,12,0,1,1-23.37,5.44,34.65,34.65,0,0,0-58.22-16.58L46.11,144.89A34.62,34.62,0,0,0,70.57,204h0a34.41,34.41,0,0,0,24.49-10.14l5.11-5.12A12,12,0,0,1,117.18,188.74ZM226.83,45.17a58.65,58.65,0,0,0-82.93,0l-5.11,5.11a12,12,0,0,0,17,17l5.12-5.12a34.63,34.63,0,1,1,49,49L175.1,145.86A34.39,34.39,0,0,1,150.61,156h0a34.63,34.63,0,0,1-33.69-26.72,12,12,0,0,0-23.38,5.44A58.64,58.64,0,0,0,150.56,180h.05a58.28,58.28,0,0,0,41.47-17.17l34.75-34.75a58.62,58.62,0,0,0,0-82.91Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M218.34,119.6,183.6,154.34a46.58,46.58,0,0,1-44.31,12.26c-.31.34-.62.67-.95,1L103.6,202.34A46.63,46.63,0,1,1,37.66,136.4L72.4,101.66A46.6,46.6,0,0,1,116.71,89.4c.31-.34.62-.67,1-1L152.4,53.66a46.63,46.63,0,0,1,65.94,65.94Z",opacity:"0.2"}),D.createElement("path",{d:"M240,88.23a54.43,54.43,0,0,1-16,37L189.25,160a54.27,54.27,0,0,1-38.63,16h-.05A54.63,54.63,0,0,1,96,119.84a8,8,0,0,1,16,.45A38.62,38.62,0,0,0,150.58,160h0a38.39,38.39,0,0,0,27.31-11.31l34.75-34.75a38.63,38.63,0,0,0-54.63-54.63l-11,11A8,8,0,0,1,135.7,59l11-11A54.65,54.65,0,0,1,224,48,54.86,54.86,0,0,1,240,88.23ZM109,185.66l-11,11A38.41,38.41,0,0,1,70.6,208h0a38.63,38.63,0,0,1-27.29-65.94L78,107.31A38.63,38.63,0,0,1,144,135.71a8,8,0,0,0,7.78,8.22H152a8,8,0,0,0,8-7.78A54.86,54.86,0,0,0,144,96a54.65,54.65,0,0,0-77.27,0L32,130.75A54.62,54.62,0,0,0,70.56,224h0a54.28,54.28,0,0,0,38.64-16l11-11A8,8,0,0,0,109,185.66Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM115.7,192.49a43.31,43.31,0,0,1-55-66.43l25.37-25.37a43.35,43.35,0,0,1,61.25,0,42.9,42.9,0,0,1,9.95,15.43,8,8,0,1,1-15,5.6A27.33,27.33,0,0,0,97.37,112L72,137.37a27.32,27.32,0,0,0,34.68,41.91,8,8,0,1,1,9,13.21Zm79.61-62.55-25.37,25.37A43,43,0,0,1,139.32,168h0a43.35,43.35,0,0,1-40.53-28.12,8,8,0,1,1,15-5.6A27.35,27.35,0,0,0,139.28,152h0a27.14,27.14,0,0,0,19.32-8L184,118.63a27.32,27.32,0,0,0-34.68-41.91,8,8,0,1,1-9-13.21,43.32,43.32,0,0,1,55,66.43Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M238,88.18a52.42,52.42,0,0,1-15.4,35.66l-34.75,34.75A52.28,52.28,0,0,1,150.62,174h-.05A52.63,52.63,0,0,1,98,119.9a6,6,0,0,1,6-5.84h.17a6,6,0,0,1,5.83,6.16A40.62,40.62,0,0,0,150.58,162h0a40.4,40.4,0,0,0,28.73-11.9l34.75-34.74A40.63,40.63,0,0,0,156.63,57.9l-11,11a6,6,0,0,1-8.49-8.49l11-11a52.62,52.62,0,0,1,74.43,0A52.83,52.83,0,0,1,238,88.18Zm-127.62,98.9-11,11A40.36,40.36,0,0,1,70.6,210h0a40.63,40.63,0,0,1-28.7-69.36L76.62,105.9A40.63,40.63,0,0,1,146,135.77a6,6,0,0,0,5.83,6.16H152a6,6,0,0,0,6-5.84A52.63,52.63,0,0,0,68.14,97.42L33.38,132.16A52.63,52.63,0,0,0,70.56,222h0a52.26,52.26,0,0,0,37.22-15.42l11-11a6,6,0,1,0-8.49-8.48Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240,88.23a54.43,54.43,0,0,1-16,37L189.25,160a54.27,54.27,0,0,1-38.63,16h-.05A54.63,54.63,0,0,1,96,119.84a8,8,0,0,1,16,.45A38.62,38.62,0,0,0,150.58,160h0a38.39,38.39,0,0,0,27.31-11.31l34.75-34.75a38.63,38.63,0,0,0-54.63-54.63l-11,11A8,8,0,0,1,135.7,59l11-11A54.65,54.65,0,0,1,224,48,54.86,54.86,0,0,1,240,88.23ZM109,185.66l-11,11A38.41,38.41,0,0,1,70.6,208h0a38.63,38.63,0,0,1-27.29-65.94L78,107.31A38.63,38.63,0,0,1,144,135.71a8,8,0,0,0,16,.45A54.86,54.86,0,0,0,144,96a54.65,54.65,0,0,0-77.27,0L32,130.75A54.62,54.62,0,0,0,70.56,224h0a54.28,54.28,0,0,0,38.64-16l11-11A8,8,0,0,0,109,185.66Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M236,88.12a50.44,50.44,0,0,1-14.81,34.31l-34.75,34.74A50.33,50.33,0,0,1,150.62,172h-.05A50.63,50.63,0,0,1,100,120a4,4,0,0,1,4-3.89h.11a4,4,0,0,1,3.89,4.11A42.64,42.64,0,0,0,150.58,164h0a42.32,42.32,0,0,0,30.14-12.49l34.75-34.74a42.63,42.63,0,1,0-60.29-60.28l-11,11a4,4,0,0,1-5.66-5.65l11-11A50.64,50.64,0,0,1,236,88.12ZM111.78,188.49l-11,11A42.33,42.33,0,0,1,70.6,212h0a42.63,42.63,0,0,1-30.11-72.77l34.75-34.74A42.63,42.63,0,0,1,148,135.82a4,4,0,0,0,8,.23A50.64,50.64,0,0,0,69.55,98.83L34.8,133.57A50.63,50.63,0,0,0,70.56,220h0a50.33,50.33,0,0,0,35.81-14.83l11-11a4,4,0,1,0-5.65-5.66Z"}))]]),m2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M228,128a12,12,0,0,1-12,12H40a12,12,0,0,1,0-24H216A12,12,0,0,1,228,128ZM40,76H216a12,12,0,0,0,0-24H40a12,12,0,0,0,0,24ZM216,180H40a12,12,0,0,0,0,24H216a12,12,0,0,0,0-24Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,64V192H40V64Z",opacity:"0.2"}),D.createElement("path",{d:"M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM192,184H64a8,8,0,0,1,0-16H192a8,8,0,0,1,0,16Zm0-48H64a8,8,0,0,1,0-16H192a8,8,0,0,1,0,16Zm0-48H64a8,8,0,0,1,0-16H192a8,8,0,0,1,0,16Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M222,128a6,6,0,0,1-6,6H40a6,6,0,0,1,0-12H216A6,6,0,0,1,222,128ZM40,70H216a6,6,0,0,0,0-12H40a6,6,0,0,0,0,12ZM216,186H40a6,6,0,0,0,0,12H216a6,6,0,0,0,0-12Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M220,128a4,4,0,0,1-4,4H40a4,4,0,0,1,0-8H216A4,4,0,0,1,220,128ZM40,68H216a4,4,0,0,0,0-8H40a4,4,0,0,0,0,8ZM216,188H40a4,4,0,0,0,0,8H216a4,4,0,0,0,0-8Z"}))]]),b2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M76,64A12,12,0,0,1,88,52H216a12,12,0,0,1,0,24H88A12,12,0,0,1,76,64Zm140,52H88a12,12,0,0,0,0,24H216a12,12,0,0,0,0-24Zm0,64H88a12,12,0,0,0,0,24H216a12,12,0,0,0,0-24ZM44,112a16,16,0,1,0,16,16A16,16,0,0,0,44,112Zm0-64A16,16,0,1,0,60,64,16,16,0,0,0,44,48Zm0,128a16,16,0,1,0,16,16A16,16,0,0,0,44,176Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,64V192H88V64Z",opacity:"0.2"}),D.createElement("path",{d:"M80,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H88A8,8,0,0,1,80,64Zm136,56H88a8,8,0,1,0,0,16H216a8,8,0,0,0,0-16Zm0,64H88a8,8,0,1,0,0,16H216a8,8,0,0,0,0-16ZM44,52A12,12,0,1,0,56,64,12,12,0,0,0,44,52Zm0,64a12,12,0,1,0,12,12A12,12,0,0,0,44,116Zm0,64a12,12,0,1,0,12,12A12,12,0,0,0,44,180Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM68,188a12,12,0,1,1,12-12A12,12,0,0,1,68,188Zm0-48a12,12,0,1,1,12-12A12,12,0,0,1,68,140Zm0-48A12,12,0,1,1,80,80,12,12,0,0,1,68,92Zm124,92H104a8,8,0,0,1,0-16h88a8,8,0,0,1,0,16Zm0-48H104a8,8,0,0,1,0-16h88a8,8,0,0,1,0,16Zm0-48H104a8,8,0,0,1,0-16h88a8,8,0,0,1,0,16Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M82,64a6,6,0,0,1,6-6H216a6,6,0,0,1,0,12H88A6,6,0,0,1,82,64Zm134,58H88a6,6,0,0,0,0,12H216a6,6,0,0,0,0-12Zm0,64H88a6,6,0,0,0,0,12H216a6,6,0,0,0,0-12ZM44,54A10,10,0,1,0,54,64,10,10,0,0,0,44,54Zm0,128a10,10,0,1,0,10,10A10,10,0,0,0,44,182Zm0-64a10,10,0,1,0,10,10A10,10,0,0,0,44,118Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M80,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H88A8,8,0,0,1,80,64Zm136,56H88a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Zm0,64H88a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM44,52A12,12,0,1,0,56,64,12,12,0,0,0,44,52Zm0,64a12,12,0,1,0,12,12A12,12,0,0,0,44,116Zm0,64a12,12,0,1,0,12,12A12,12,0,0,0,44,180Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M84,64a4,4,0,0,1,4-4H216a4,4,0,0,1,0,8H88A4,4,0,0,1,84,64Zm132,60H88a4,4,0,0,0,0,8H216a4,4,0,0,0,0-8Zm0,64H88a4,4,0,0,0,0,8H216a4,4,0,0,0,0-8ZM44,120a8,8,0,1,0,8,8A8,8,0,0,0,44,120Zm0-64a8,8,0,1,0,8,8A8,8,0,0,0,44,56Zm0,128a8,8,0,1,0,8,8A8,8,0,0,0,44,184Z"}))]]),v2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M28,64A12,12,0,0,1,40,52H216a12,12,0,0,1,0,24H40A12,12,0,0,1,28,64Zm12,76H216a12,12,0,0,0,0-24H40a12,12,0,0,0,0,24Zm104,40H40a12,12,0,0,0,0,24H144a12,12,0,0,0,0-24Zm88,0H220V168a12,12,0,0,0-24,0v12H184a12,12,0,0,0,0,24h12v12a12,12,0,0,0,24,0V204h12a12,12,0,0,0,0-24Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,64V192H40V64Z",opacity:"0.2"}),D.createElement("path",{d:"M32,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H40A8,8,0,0,1,32,64Zm8,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16Zm104,48H40a8,8,0,0,0,0,16H144a8,8,0,0,0,0-16Zm88,0H216V168a8,8,0,0,0-16,0v16H184a8,8,0,0,0,0,16h16v16a8,8,0,0,0,16,0V200h16a8,8,0,0,0,0-16Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM64,72H192a8,8,0,0,1,0,16H64a8,8,0,0,1,0-16Zm56,112H64a8,8,0,0,1,0-16h56a8,8,0,0,1,0,16Zm16-48H64a8,8,0,0,1,0-16h72a8,8,0,0,1,0,16Zm64,32H184v16a8,8,0,0,1-16,0V168H152a8,8,0,0,1,0-16h16V136a8,8,0,0,1,16,0v16h16a8,8,0,0,1,0,16Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M34,64a6,6,0,0,1,6-6H216a6,6,0,0,1,0,12H40A6,6,0,0,1,34,64Zm6,70H216a6,6,0,0,0,0-12H40a6,6,0,0,0,0,12Zm104,52H40a6,6,0,0,0,0,12H144a6,6,0,0,0,0-12Zm88,0H214V168a6,6,0,0,0-12,0v18H184a6,6,0,0,0,0,12h18v18a6,6,0,0,0,12,0V198h18a6,6,0,0,0,0-12Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M32,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H40A8,8,0,0,1,32,64Zm8,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16Zm104,48H40a8,8,0,0,0,0,16H144a8,8,0,0,0,0-16Zm88,0H216V168a8,8,0,0,0-16,0v16H184a8,8,0,0,0,0,16h16v16a8,8,0,0,0,16,0V200h16a8,8,0,0,0,0-16Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M36,64a4,4,0,0,1,4-4H216a4,4,0,0,1,0,8H40A4,4,0,0,1,36,64Zm4,68H216a4,4,0,0,0,0-8H40a4,4,0,0,0,0,8Zm104,56H40a4,4,0,0,0,0,8H144a4,4,0,0,0,0-8Zm88,0H212V168a4,4,0,0,0-8,0v20H184a4,4,0,0,0,0,8h20v20a4,4,0,0,0,8,0V196h20a4,4,0,0,0,0-8Z"}))]]),w2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232.49,215.51,185,168a92.12,92.12,0,1,0-17,17l47.53,47.54a12,12,0,0,0,17-17ZM44,112a68,68,0,1,1,68,68A68.07,68.07,0,0,1,44,112Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M192,112a80,80,0,1,1-80-80A80,80,0,0,1,192,112Z",opacity:"0.2"}),D.createElement("path",{d:"M229.66,218.34,179.6,168.28a88.21,88.21,0,1,0-11.32,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M168,112a56,56,0,1,1-56-56A56,56,0,0,1,168,112Zm61.66,117.66a8,8,0,0,1-11.32,0l-50.06-50.07a88,88,0,1,1,11.32-11.31l50.06,50.06A8,8,0,0,1,229.66,229.66ZM112,184a72,72,0,1,0-72-72A72.08,72.08,0,0,0,112,184Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M228.24,219.76l-51.38-51.38a86.15,86.15,0,1,0-8.48,8.48l51.38,51.38a6,6,0,0,0,8.48-8.48ZM38,112a74,74,0,1,1,74,74A74.09,74.09,0,0,1,38,112Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M226.83,221.17l-52.7-52.7a84.1,84.1,0,1,0-5.66,5.66l52.7,52.7a4,4,0,0,0,5.66-5.66ZM36,112a76,76,0,1,1,76,76A76.08,76.08,0,0,1,36,112Z"}))]]),y2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232.49,55.51l-32-32a12,12,0,0,0-17,0l-96,96A12,12,0,0,0,84,128v32a12,12,0,0,0,12,12h32a12,12,0,0,0,8.49-3.51l96-96A12,12,0,0,0,232.49,55.51ZM192,49l15,15L196,75,181,60Zm-69,99H108V133l56-56,15,15Zm105-7.43V208a20,20,0,0,1-20,20H48a20,20,0,0,1-20-20V48A20,20,0,0,1,48,28h67.43a12,12,0,0,1,0,24H52V204H204V140.57a12,12,0,0,1,24,0Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,88l-72,72H96V128l72-72Z",opacity:"0.2"}),D.createElement("path",{d:"M229.66,58.34l-32-32a8,8,0,0,0-11.32,0l-96,96A8,8,0,0,0,88,128v32a8,8,0,0,0,8,8h32a8,8,0,0,0,5.66-2.34l96-96A8,8,0,0,0,229.66,58.34ZM124.69,152H104V131.31l64-64L188.69,88ZM200,76.69,179.31,56,192,43.31,212.69,64ZM224,128v80a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V48A16,16,0,0,1,48,32h80a8,8,0,0,1,0,16H48V208H208V128a8,8,0,0,1,16,0Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128v80a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V48A16,16,0,0,1,48,32h80a8,8,0,0,1,0,16H48V208H208V128a8,8,0,0,1,16,0Zm5.66-58.34-96,96A8,8,0,0,1,128,168H96a8,8,0,0,1-8-8V128a8,8,0,0,1,2.34-5.66l96-96a8,8,0,0,1,11.32,0l32,32A8,8,0,0,1,229.66,69.66Zm-17-5.66L192,43.31,179.31,56,200,76.69Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M228.24,59.76l-32-32a6,6,0,0,0-8.48,0l-96,96A6,6,0,0,0,90,128v32a6,6,0,0,0,6,6h32a6,6,0,0,0,4.24-1.76l96-96A6,6,0,0,0,228.24,59.76ZM125.51,154H102V130.49l66-66L191.51,88ZM200,79.51,176.49,56,192,40.49,215.51,64ZM222,128v80a14,14,0,0,1-14,14H48a14,14,0,0,1-14-14V48A14,14,0,0,1,48,34h80a6,6,0,0,1,0,12H48a2,2,0,0,0-2,2V208a2,2,0,0,0,2,2H208a2,2,0,0,0,2-2V128a6,6,0,0,1,12,0Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M229.66,58.34l-32-32a8,8,0,0,0-11.32,0l-96,96A8,8,0,0,0,88,128v32a8,8,0,0,0,8,8h32a8,8,0,0,0,5.66-2.34l96-96A8,8,0,0,0,229.66,58.34ZM124.69,152H104V131.31l64-64L188.69,88ZM200,76.69,179.31,56,192,43.31,212.69,64ZM224,128v80a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V48A16,16,0,0,1,48,32h80a8,8,0,0,1,0,16H48V208H208V128a8,8,0,0,1,16,0Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M226.83,61.17l-32-32a4,4,0,0,0-5.66,0l-96,96A4,4,0,0,0,92,128v32a4,4,0,0,0,4,4h32a4,4,0,0,0,2.83-1.17l96-96A4,4,0,0,0,226.83,61.17ZM126.34,156H100V129.66l68-68L194.34,88ZM200,82.34,173.66,56,192,37.66,218.34,64ZM220,128v80a12,12,0,0,1-12,12H48a12,12,0,0,1-12-12V48A12,12,0,0,1,48,36h80a4,4,0,0,1,0,8H48a4,4,0,0,0-4,4V208a4,4,0,0,0,4,4H208a4,4,0,0,0,4-4V128a4,4,0,0,1,8,0Z"}))]]),_2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,28H160a20,20,0,0,0-20,20V208a20,20,0,0,0,20,20h40a20,20,0,0,0,20-20V48A20,20,0,0,0,200,28Zm-4,176H164V52h32ZM96,28H56A20,20,0,0,0,36,48V208a20,20,0,0,0,20,20H96a20,20,0,0,0,20-20V48A20,20,0,0,0,96,28ZM92,204H60V52H92Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,48V208a8,8,0,0,1-8,8H160a8,8,0,0,1-8-8V48a8,8,0,0,1,8-8h40A8,8,0,0,1,208,48ZM96,40H56a8,8,0,0,0-8,8V208a8,8,0,0,0,8,8H96a8,8,0,0,0,8-8V48A8,8,0,0,0,96,40Z",opacity:"0.2"}),D.createElement("path",{d:"M200,32H160a16,16,0,0,0-16,16V208a16,16,0,0,0,16,16h40a16,16,0,0,0,16-16V48A16,16,0,0,0,200,32Zm0,176H160V48h40ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Zm0,176H56V48H96Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,48V208a16,16,0,0,1-16,16H160a16,16,0,0,1-16-16V48a16,16,0,0,1,16-16h40A16,16,0,0,1,216,48ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,34H160a14,14,0,0,0-14,14V208a14,14,0,0,0,14,14h40a14,14,0,0,0,14-14V48A14,14,0,0,0,200,34Zm2,174a2,2,0,0,1-2,2H160a2,2,0,0,1-2-2V48a2,2,0,0,1,2-2h40a2,2,0,0,1,2,2ZM96,34H56A14,14,0,0,0,42,48V208a14,14,0,0,0,14,14H96a14,14,0,0,0,14-14V48A14,14,0,0,0,96,34Zm2,174a2,2,0,0,1-2,2H56a2,2,0,0,1-2-2V48a2,2,0,0,1,2-2H96a2,2,0,0,1,2,2Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,32H160a16,16,0,0,0-16,16V208a16,16,0,0,0,16,16h40a16,16,0,0,0,16-16V48A16,16,0,0,0,200,32Zm0,176H160V48h40ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Zm0,176H56V48H96Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,36H160a12,12,0,0,0-12,12V208a12,12,0,0,0,12,12h40a12,12,0,0,0,12-12V48A12,12,0,0,0,200,36Zm4,172a4,4,0,0,1-4,4H160a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4h40a4,4,0,0,1,4,4ZM96,36H56A12,12,0,0,0,44,48V208a12,12,0,0,0,12,12H96a12,12,0,0,0,12-12V48A12,12,0,0,0,96,36Zm4,172a4,4,0,0,1-4,4H56a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H96a4,4,0,0,1,4,4Z"}))]]),C2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212ZM116,96v64a12,12,0,0,1-24,0V96a12,12,0,0,1,24,0Zm48,0v64a12,12,0,0,1-24,0V96a12,12,0,0,1,24,0Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z",opacity:"0.2"}),D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216ZM112,96v64a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.13,104.13,0,0,0,128,24ZM112,160a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0Zm48,0a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,26A102,102,0,1,0,230,128,102.12,102.12,0,0,0,128,26Zm0,192a90,90,0,1,1,90-90A90.1,90.1,0,0,1,128,218ZM110,96v64a6,6,0,0,1-12,0V96a6,6,0,0,1,12,0Zm48,0v64a6,6,0,0,1-12,0V96a6,6,0,0,1,12,0Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216ZM112,96v64a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,28A100,100,0,1,0,228,128,100.11,100.11,0,0,0,128,28Zm0,192a92,92,0,1,1,92-92A92.1,92.1,0,0,1,128,220ZM108,96v64a4,4,0,0,1-8,0V96a4,4,0,0,1,8,0Zm48,0v64a4,4,0,0,1-8,0V96a4,4,0,0,1,8,0Z"}))]]),S2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M230.14,70.54,185.46,25.85a20,20,0,0,0-28.29,0L33.86,149.17A19.85,19.85,0,0,0,28,163.31V208a20,20,0,0,0,20,20H92.69a19.86,19.86,0,0,0,14.14-5.86L230.14,98.82a20,20,0,0,0,0-28.28ZM91,204H52V165l84-84,39,39ZM192,103,153,64l18.34-18.34,39,39Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M221.66,90.34,192,120,136,64l29.66-29.66a8,8,0,0,1,11.31,0L221.66,79A8,8,0,0,1,221.66,90.34Z",opacity:"0.2"}),D.createElement("path",{d:"M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM192,108.68,147.31,64l24-24L216,84.68Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M225.9,74.78,181.21,30.09a14,14,0,0,0-19.8,0L38.1,153.41a13.94,13.94,0,0,0-4.1,9.9V208a14,14,0,0,0,14,14H92.69a13.94,13.94,0,0,0,9.9-4.1L225.9,94.58a14,14,0,0,0,0-19.8ZM94.1,209.41a2,2,0,0,1-1.41.59H48a2,2,0,0,1-2-2V163.31a2,2,0,0,1,.59-1.41L136,72.48,183.51,120ZM217.41,86.1,192,111.51,144.49,64,169.9,38.58a2,2,0,0,1,2.83,0l44.68,44.69a2,2,0,0,1,0,2.83Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224.49,76.2,179.8,31.51a12,12,0,0,0-17,0L133.17,61.17h0L39.52,154.83A11.9,11.9,0,0,0,36,163.31V208a12,12,0,0,0,12,12H92.69a12,12,0,0,0,8.48-3.51L224.48,93.17a12,12,0,0,0,0-17Zm-129,134.63A4,4,0,0,1,92.69,212H48a4,4,0,0,1-4-4V163.31a4,4,0,0,1,1.17-2.83L136,69.65,186.34,120ZM218.83,87.51,192,114.34,141.66,64l26.82-26.83a4,4,0,0,1,5.66,0l44.69,44.68a4,4,0,0,1,0,5.66Z"}))]]),x2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M221.29,131.34,176.17,80.19A36,36,0,0,0,150.55,68a36,36,0,1,0-45.1,0A36,36,0,0,0,79.83,80.19L34.71,131.34a24,24,0,0,0,33.7,34.16l6.73-5.39L61.74,211a24,24,0,0,0,43.74,19.69L128,191.9l22.52,38.79a23.82,23.82,0,0,0,13.27,11.85A24,24,0,0,0,194.26,211l-13.4-50.89,6.73,5.39a24,24,0,0,0,33.7-34.16ZM128,28a12,12,0,1,1-12,12A12,12,0,0,1,128,28Zm75,119.12-35.52-28.49a12,12,0,0,0-19.11,12.42L171.27,218a12.18,12.18,0,0,0,.73,2,10.72,10.72,0,0,0-.5-1L138.38,162a12,12,0,0,0-20.76,0L84.5,219a10.72,10.72,0,0,0-.5,1,13.16,13.16,0,0,0,.73-2l22.87-86.92a12,12,0,0,0-19.11-12.42L53,147.12a11.5,11.5,0,0,0-1,.87c.18-.17.35-.36.52-.54L97.83,96.06a12,12,0,0,1,9-4.06h42.34a12,12,0,0,1,9,4.06l45.32,51.39c.17.18.34.37.52.54A11.5,11.5,0,0,0,203,147.12Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M104,40a24,24,0,1,1,24,24A24,24,0,0,1,104,40Zm108.49,99.51L167.17,88.13a24,24,0,0,0-18-8.13H106.83a24,24,0,0,0-18,8.13L43.51,139.51a12,12,0,0,0,17,17L96,128,73.13,214.93a12,12,0,0,0,21.75,10.14L128,168l33.12,57.07a12,12,0,0,0,21.75-10.14L160,128l35.51,28.49a12,12,0,0,0,17-17Z",opacity:"0.2"}),D.createElement("path",{d:"M160,40a32,32,0,1,0-32,32A32,32,0,0,0,160,40ZM128,56a16,16,0,1,1,16-16A16,16,0,0,1,128,56Zm90.34,78.05L173.17,82.83a32,32,0,0,0-24-10.83H106.83a32,32,0,0,0-24,10.83L37.66,134.05a20,20,0,0,0,28.13,28.43l16.3-13.08L65.55,212.28A20,20,0,0,0,102,228.8l26-44.87,26,44.87a20,20,0,0,0,36.41-16.52L173.91,149.4l16.3,13.08a20,20,0,0,0,28.13-28.43Zm-11.51,16.77a4,4,0,0,1-5.66,0c-.21-.2-.42-.4-.65-.58L165,121.76A8,8,0,0,0,152.26,130L175.14,217a7.72,7.72,0,0,0,.48,1.35,4,4,0,1,1-7.25,3.38,6.25,6.25,0,0,0-.33-.63L134.92,164a8,8,0,0,0-13.84,0L88,221.05a6.25,6.25,0,0,0-.33.63,4,4,0,0,1-2.26,2.07,4,4,0,0,1-5-5.45,7.72,7.72,0,0,0,.48-1.35L103.74,130A8,8,0,0,0,91,121.76L55.48,150.24c-.23.18-.44.38-.65.58a4,4,0,1,1-5.66-5.65c.12-.12.23-.24.34-.37L94.83,93.41a16,16,0,0,1,12-5.41h42.34a16,16,0,0,1,12,5.41l45.32,51.39c.11.13.22.25.34.37A4,4,0,0,1,206.83,150.82Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M100,36a28,28,0,1,1,28,28A28,28,0,0,1,100,36ZM215.42,140.78l-45.25-51.3a28,28,0,0,0-21-9.48H106.83a28,28,0,0,0-21,9.48l-45.25,51.3a16,16,0,0,0,22.56,22.69L89,142.7l-19.7,74.88a16,16,0,0,0,29.08,13.35L128,180l29.58,51a16,16,0,0,0,29.08-13.35L167,142.7l25.9,20.77a16,16,0,0,0,22.56-22.69Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,70A30,30,0,1,0,98,40,30,30,0,0,0,128,70Zm0-48a18,18,0,1,1-18,18A18,18,0,0,1,128,22Zm88.88,113.42L171.67,84.16A30,30,0,0,0,149.17,74H106.83a30,30,0,0,0-22.5,10.15L39.12,135.42A18,18,0,0,0,64.46,161l21.11-16.93L67.44,212.92a18,18,0,0,0,32.75,14.94L128,180l27.81,47.91a18,18,0,0,0,32.75-14.94l-18.13-68.87L191.54,161a18,18,0,0,0,25.34-25.56Zm-8.63,16.82a6,6,0,0,1-8.49,0,4.15,4.15,0,0,0-.49-.44l-35.51-28.48a6,6,0,0,0-9.56,6.2l22.87,86.93a7.66,7.66,0,0,0,.37,1,6,6,0,0,1-10.88,5.07,4.37,4.37,0,0,0-.25-.48L133.19,165a6,6,0,0,0-10.38,0L89.69,222.05a4.37,4.37,0,0,0-.25.48,6,6,0,0,1-10.88-5.07,7.66,7.66,0,0,0,.37-1l22.87-86.93A6,6,0,0,0,99.27,123,6.07,6.07,0,0,0,96,122a6,6,0,0,0-3.76,1.32L56.73,151.8a4.15,4.15,0,0,0-.49.44,6,6,0,0,1-8.49-8.49l.26-.27L93.33,92.09A18,18,0,0,1,106.83,86h42.34a18,18,0,0,1,13.5,6.09L208,143.48l.26.27A6,6,0,0,1,208.25,152.24Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M160,40a32,32,0,1,0-32,32A32,32,0,0,0,160,40ZM128,56a16,16,0,1,1,16-16A16,16,0,0,1,128,56Zm90.34,78.05L173.17,82.83a32,32,0,0,0-24-10.83H106.83a32,32,0,0,0-24,10.83L37.66,134.05a20,20,0,0,0,28.13,28.43l16.3-13.08L65.55,212.28A20,20,0,0,0,102,228.8l26-44.87,26,44.87a20,20,0,0,0,36.41-16.52L173.91,149.4l16.3,13.08a20,20,0,0,0,28.13-28.43Zm-11.51,16.77a4,4,0,0,1-5.66,0c-.21-.2-.42-.4-.65-.58L165,121.76A8,8,0,0,0,152.26,130L175.14,217a7.72,7.72,0,0,0,.48,1.35,4,4,0,1,1-7.25,3.38,6.25,6.25,0,0,0-.33-.63L134.92,164a8,8,0,0,0-13.84,0L88,221.05a6.25,6.25,0,0,0-.33.63,4,4,0,0,1-2.26,2.07,4,4,0,0,1-5-5.45,7.72,7.72,0,0,0,.48-1.35L103.74,130A8,8,0,0,0,91,121.76L55.48,150.24c-.23.18-.44.38-.65.58a4,4,0,1,1-5.66-5.65c.12-.12.23-.24.34-.37L94.83,93.41a16,16,0,0,1,12-5.41h42.34a16,16,0,0,1,12,5.41l45.32,51.39c.11.13.22.25.34.37A4,4,0,0,1,206.83,150.82Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,68a28,28,0,1,0-28-28A28,28,0,0,0,128,68Zm0-48a20,20,0,1,1-20,20A20,20,0,0,1,128,20Zm87.42,116.78-45.25-51.3a28,28,0,0,0-21-9.48H106.83a28,28,0,0,0-21,9.48l-45.25,51.3a16,16,0,0,0,22.56,22.69L89,138.7l-19.7,74.88a16,16,0,0,0,29.08,13.35L128,176l29.58,51a16,16,0,0,0,29.08-13.35L167,138.7l25.9,20.77a16,16,0,0,0,22.56-22.69Zm-5.76,16.87a8,8,0,0,1-11.31,0,3.11,3.11,0,0,0-.33-.29l-35.51-28.48a4,4,0,0,0-6.38,4.13L179,215.94a4.12,4.12,0,0,0,.24.67,8,8,0,1,1-14.5,6.76c-.05-.11-.11-.21-.17-.32L131.46,166a4,4,0,0,0-6.92,0L91.42,223.05c-.06.11-.12.21-.17.32a8,8,0,1,1-14.5-6.76,4.12,4.12,0,0,0,.24-.67L99.87,129a4,4,0,0,0-6.38-4.13L58,153.36a3.11,3.11,0,0,0-.33.29,8,8,0,0,1-11.31-11.31l.17-.18L91.83,90.77a20,20,0,0,1,15-6.77h42.34a20,20,0,0,1,15,6.77l45.32,51.39.17.18A8,8,0,0,1,209.66,153.65Z"}))]]),E2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M152,92a36,36,0,1,0-36-36A36,36,0,0,0,152,92Zm0-48a12,12,0,1,1-12,12A12,12,0,0,1,152,44Zm76,93.4a12,12,0,0,1-7,10.91,66,66,0,0,1-21.47,3.78c-14,0-34.25-3.82-59.77-19a177,177,0,0,1-10.27,21C153.12,162.83,188,183.8,188,232a12,12,0,0,1-24,0c0-18.69-6.95-33.06-21.26-43.94-9.16-7-19.55-11-27.43-13.34-.81,1-1.64,2-2.5,2.95-20,22.87-44.82,34.76-72.25,34.76a97.33,97.33,0,0,1-9.75-.49,12,12,0,1,1,2.39-23.88c52.3,5.22,77.48-45.92,85.79-67.75C84.8,102.46,63.74,118.78,63.51,119a12,12,0,0,1-15-18.72C50.08,99,88,69.44,142.75,106.62c43.1,29.31,68.1,19.92,68.5,19.76a12,12,0,0,1,16.75,11Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M176,56a24,24,0,1,1-24-24A24,24,0,0,1,176,56Z",opacity:"0.2"}),D.createElement("path",{d:"M152,88a32,32,0,1,0-32-32A32,32,0,0,0,152,88Zm0-48a16,16,0,1,1-16,16A16,16,0,0,1,152,40Zm67.31,100.68c-.61.28-7.49,3.28-19.67,3.28-13.85,0-34.55-3.88-60.69-20a169.31,169.31,0,0,1-15.41,32.34,104.29,104.29,0,0,1,31.31,15.81C173.92,186.65,184,207.35,184,232a8,8,0,0,1-16,0c0-41.7-34.69-56.71-54.14-61.85-.55.7-1.12,1.41-1.69,2.1-19.64,23.8-44.25,36.18-71.63,36.18A92.29,92.29,0,0,1,31.2,208,8,8,0,0,1,32.8,192c25.92,2.59,48.47-7.49,67-30,12.49-15.14,21-33.61,25.25-47C86.13,92.34,61.27,111.63,61,111.84A8,8,0,1,1,51,99.36c1.5-1.2,37.22-29,89.51,6.57,45.47,30.91,71.93,20.31,72.18,20.19a8,8,0,1,1,6.63,14.56Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M120,56a32,32,0,1,1,32,32A32,32,0,0,1,120,56Zm103.28,74.08a8,8,0,0,0-10.6-4c-.25.12-26.71,10.72-72.18-20.19-52.29-35.54-88-7.77-89.51-6.57a8,8,0,1,0,10,12.48c.26-.21,25.12-19.5,64.07,3.27-4.25,13.35-12.76,31.82-25.25,47-18.56,22.48-41.11,32.56-67,30A8,8,0,0,0,31.2,208a92.29,92.29,0,0,0,9.34.47c27.38,0,52-12.38,71.63-36.18.57-.69,1.14-1.4,1.69-2.1C133.31,175.29,168,190.3,168,232a8,8,0,0,0,16,0c0-24.65-10.08-45.35-29.15-59.86a104.29,104.29,0,0,0-31.31-15.81A169.31,169.31,0,0,0,139,124c26.14,16.09,46.84,20,60.69,20,12.18,0,19.06-3,19.67-3.28A8,8,0,0,0,223.28,130.08Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M152,86a30,30,0,1,0-30-30A30,30,0,0,0,152,86Zm0-48a18,18,0,1,1-18,18A18,18,0,0,1,152,38Zm66.49,100.86c-.59.27-7.17,3.13-18.88,3.13-13.86,0-34.9-4-61.73-21a165.89,165.89,0,0,1-17.43,36.51c9.43,2.78,22,7.72,33.19,16.26C172.46,188.05,182,207.65,182,232a6,6,0,0,1-12,0c0-44-37.23-59.18-56.91-64.11q-1.2,1.55-2.46,3.09c-19.25,23.31-43.34,35.45-70.11,35.45A90.72,90.72,0,0,1,31.4,206,6,6,0,0,1,32.6,194c26.63,2.66,49.77-7.66,68.77-30.69,13.16-15.94,21.94-35.51,26.08-49.15-40.51-24.52-66.59-4.78-67.72-3.89a6,6,0,0,1-7.48-9.38c.37-.3,9.39-7.43,24.76-10,13.86-2.31,35.92-1.3,62.36,16.67,47.14,32,73.88,20.47,74.14,20.35a6,6,0,1,1,5,10.92Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M152,88a32,32,0,1,0-32-32A32,32,0,0,0,152,88Zm0-48a16,16,0,1,1-16,16A16,16,0,0,1,152,40Zm67.31,100.68c-.61.28-7.49,3.28-19.67,3.28-13.85,0-34.55-3.88-60.69-20a169.31,169.31,0,0,1-15.41,32.34,104.29,104.29,0,0,1,31.31,15.81C173.92,186.65,184,207.35,184,232a8,8,0,0,1-16,0c0-41.7-34.69-56.71-54.14-61.85-.55.7-1.12,1.41-1.69,2.1-19.64,23.8-44.25,36.18-71.63,36.18A92.29,92.29,0,0,1,31.2,208,8,8,0,0,1,32.8,192c25.92,2.58,48.47-7.49,67-30,12.49-15.14,21-33.61,25.25-47C86.13,92.35,61.27,111.63,61,111.84A8,8,0,1,1,51,99.36c1.5-1.2,37.22-29,89.51,6.57,45.47,30.91,71.93,20.31,72.18,20.19a8,8,0,1,1,6.63,14.56Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M152,84a28,28,0,1,0-28-28A28,28,0,0,0,152,84Zm0-48a20,20,0,1,1-20,20A20,20,0,0,1,152,36Zm65.66,101c-.57.26-6.84,3-18.08,3-13.86,0-35.25-4.15-62.81-22.16a162.59,162.59,0,0,1-19.49,40.78c9.47,2.56,23.08,7.5,35.14,16.67,18.3,13.92,27.58,33,27.58,56.68a4,4,0,0,1-8,0c0-15.89-5.88-53.77-59.7-66.37q-1.56,2.06-3.22,4.08c-18.85,22.83-42.42,34.72-68.6,34.72q-4.4,0-8.89-.45a4,4,0,1,1,.8-8c27.33,2.73,51.06-7.83,70.52-31.41,13.82-16.74,22.89-37.44,26.9-51.32-42.84-26.69-71-4.8-71.32-4.57a4,4,0,1,1-5-6.24c.36-.29,9-7.1,23.84-9.58,13.5-2.27,35-1.26,60.91,16.34,25,17,44.41,21.64,56.29,22.56,12.75,1,19.77-2,19.84-2.05a4,4,0,0,1,3.29,7.29Z"}))]]),k2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M234.49,111.07,90.41,22.94A20,20,0,0,0,60,39.87V216.13a20,20,0,0,0,30.41,16.93l144.08-88.13a19.82,19.82,0,0,0,0-33.86ZM84,208.85V47.15L216.16,128Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M228.23,134.69,84.15,222.81A8,8,0,0,1,72,216.12V39.88a8,8,0,0,1,12.15-6.69l144.08,88.12A7.82,7.82,0,0,1,228.23,134.69Z",opacity:"0.2"}),D.createElement("path",{d:"M232.4,114.49,88.32,26.35a16,16,0,0,0-16.2-.3A15.86,15.86,0,0,0,64,39.87V216.13A15.94,15.94,0,0,0,80,232a16.07,16.07,0,0,0,8.36-2.35L232.4,141.51a15.81,15.81,0,0,0,0-27ZM80,215.94V40l143.83,88Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240,128a15.74,15.74,0,0,1-7.6,13.51L88.32,229.65a16,16,0,0,1-16.2.3A15.86,15.86,0,0,1,64,216.13V39.87a15.86,15.86,0,0,1,8.12-13.82,16,16,0,0,1,16.2.3L232.4,114.49A15.74,15.74,0,0,1,240,128Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M231.36,116.19,87.28,28.06a14,14,0,0,0-14.18-.27A13.69,13.69,0,0,0,66,39.87V216.13a13.69,13.69,0,0,0,7.1,12.08,14,14,0,0,0,14.18-.27l144.08-88.13a13.82,13.82,0,0,0,0-23.62Zm-6.26,13.38L81,217.7a2,2,0,0,1-2.06,0,1.78,1.78,0,0,1-1-1.61V39.87a1.78,1.78,0,0,1,1-1.61A2.06,2.06,0,0,1,80,38a2,2,0,0,1,1,.31L225.1,126.43a1.82,1.82,0,0,1,0,3.14Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232.4,114.49,88.32,26.35a16,16,0,0,0-16.2-.3A15.86,15.86,0,0,0,64,39.87V216.13A15.94,15.94,0,0,0,80,232a16.07,16.07,0,0,0,8.36-2.35L232.4,141.51a15.81,15.81,0,0,0,0-27ZM80,215.94V40l143.83,88Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M230.32,117.9,86.24,29.79a11.91,11.91,0,0,0-12.17-.23A11.71,11.71,0,0,0,68,39.89V216.11a11.71,11.71,0,0,0,6.07,10.33,11.91,11.91,0,0,0,12.17-.23L230.32,138.1a11.82,11.82,0,0,0,0-20.2Zm-4.18,13.37L82.06,219.39a4,4,0,0,1-4.07.07,3.77,3.77,0,0,1-2-3.35V39.89a3.77,3.77,0,0,1,2-3.35,4,4,0,0,1,4.07.07l144.08,88.12a3.8,3.8,0,0,1,0,6.54Z"}))]]),T2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M228,128a12,12,0,0,1-12,12H140v76a12,12,0,0,1-24,0V140H40a12,12,0,0,1,0-24h76V40a12,12,0,0,1,24,0v76h76A12,12,0,0,1,228,128Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,56V200a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V56A16,16,0,0,1,56,40H200A16,16,0,0,1,216,56Z",opacity:"0.2"}),D.createElement("path",{d:"M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM184,136H136v48a8,8,0,0,1-16,0V136H72a8,8,0,0,1,0-16h48V72a8,8,0,0,1,16,0v48h48a8,8,0,0,1,0,16Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M222,128a6,6,0,0,1-6,6H134v82a6,6,0,0,1-12,0V134H40a6,6,0,0,1,0-12h82V40a6,6,0,0,1,12,0v82h82A6,6,0,0,1,222,128Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M220,128a4,4,0,0,1-4,4H132v84a4,4,0,0,1-8,0V132H40a4,4,0,0,1,0-8h84V40a4,4,0,0,1,8,0v84h84A4,4,0,0,1,220,128Z"}))]]),L2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm84,108a83.6,83.6,0,0,1-16.75,50.28L77.72,60.75A84,84,0,0,1,212,128ZM44,128A83.6,83.6,0,0,1,60.75,77.72L178.28,195.25A84,84,0,0,1,44,128Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z",opacity:"0.2"}),D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm88,104a87.56,87.56,0,0,1-20.41,56.28L71.72,60.4A88,88,0,0,1,216,128ZM40,128A87.56,87.56,0,0,1,60.41,71.72L184.28,195.6A88,88,0,0,1,40,128Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,128a71.69,71.69,0,0,1-15.78,44.91L83.09,71.78A71.95,71.95,0,0,1,200,128ZM56,128a71.95,71.95,0,0,0,116.91,56.22L71.78,83.09A71.69,71.69,0,0,0,56,128Zm180,0A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-20,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,26A102,102,0,1,0,230,128,102.12,102.12,0,0,0,128,26Zm90,102a89.6,89.6,0,0,1-22.29,59.22L68.78,60.29A89.95,89.95,0,0,1,218,128ZM38,128A89.6,89.6,0,0,1,60.29,68.78L187.22,195.71A89.95,89.95,0,0,1,38,128Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm88,104a87.56,87.56,0,0,1-20.41,56.28L71.72,60.4A88,88,0,0,1,216,128ZM40,128A87.56,87.56,0,0,1,60.41,71.72L184.28,195.6A88,88,0,0,1,40,128Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,28A100,100,0,1,0,228,128,100.11,100.11,0,0,0,128,28Zm92,100a91.67,91.67,0,0,1-24.21,62.13L65.87,60.21A92,92,0,0,1,220,128ZM36,128A91.67,91.67,0,0,1,60.21,65.87L190.13,195.79A92,92,0,0,1,36,128Z"}))]]),D2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M20,128A76.08,76.08,0,0,1,96,52h99l-3.52-3.51a12,12,0,1,1,17-17l24,24a12,12,0,0,1,0,17l-24,24a12,12,0,0,1-17-17L195,76H96a52.06,52.06,0,0,0-52,52,12,12,0,0,1-24,0Zm204-12a12,12,0,0,0-12,12,52.06,52.06,0,0,1-52,52H61l3.52-3.51a12,12,0,1,0-17-17l-24,24a12,12,0,0,0,0,17l24,24a12,12,0,1,0,17-17L61,204h99a76.08,76.08,0,0,0,76-76A12,12,0,0,0,224,116Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,64v64a64,64,0,0,1-64,64H32V128A64,64,0,0,1,96,64Z",opacity:"0.2"}),D.createElement("path",{d:"M24,128A72.08,72.08,0,0,1,96,56H204.69L194.34,45.66a8,8,0,0,1,11.32-11.32l24,24a8,8,0,0,1,0,11.32l-24,24a8,8,0,0,1-11.32-11.32L204.69,72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H51.31l10.35-10.34a8,8,0,0,0-11.32-11.32l-24,24a8,8,0,0,0,0,11.32l24,24a8,8,0,0,0,11.32-11.32L51.31,200H160a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M26,128A70.08,70.08,0,0,1,96,58H209.51L195.76,44.24a6,6,0,0,1,8.48-8.48l24,24a6,6,0,0,1,0,8.48l-24,24a6,6,0,0,1-8.48-8.48L209.51,70H96a58.07,58.07,0,0,0-58,58,6,6,0,0,1-12,0Zm198-6a6,6,0,0,0-6,6,58.07,58.07,0,0,1-58,58H46.49l13.75-13.76a6,6,0,0,0-8.48-8.48l-24,24a6,6,0,0,0,0,8.48l24,24a6,6,0,0,0,8.48-8.48L46.49,198H160a70.08,70.08,0,0,0,70-70A6,6,0,0,0,224,122Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M24,128A72.08,72.08,0,0,1,96,56H204.69L194.34,45.66a8,8,0,0,1,11.32-11.32l24,24a8,8,0,0,1,0,11.32l-24,24a8,8,0,0,1-11.32-11.32L204.69,72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H51.31l10.35-10.34a8,8,0,0,0-11.32-11.32l-24,24a8,8,0,0,0,0,11.32l24,24a8,8,0,0,0,11.32-11.32L51.31,200H160a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M28,128A68.07,68.07,0,0,1,96,60H214.34L197.17,42.83a4,4,0,0,1,5.66-5.66l24,24a4,4,0,0,1,0,5.66l-24,24a4,4,0,0,1-5.66-5.66L214.34,68H96a60.07,60.07,0,0,0-60,60,4,4,0,0,1-8,0Zm196-4a4,4,0,0,0-4,4,60.07,60.07,0,0,1-60,60H41.66l17.17-17.17a4,4,0,0,0-5.66-5.66l-24,24a4,4,0,0,0,0,5.66l24,24a4,4,0,1,0,5.66-5.66L41.66,196H160a68.07,68.07,0,0,0,68-68A4,4,0,0,0,224,124Z"}))]]),I2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M72,104a16,16,0,1,1,16,16A16,16,0,0,1,72,104Zm96,16a16,16,0,1,0-16-16A16,16,0,0,0,168,120Zm68-40V192a36,36,0,0,1-36,36H56a36,36,0,0,1-36-36V80A36,36,0,0,1,56,44h60V16a12,12,0,0,1,24,0V44h60A36,36,0,0,1,236,80Zm-24,0a12,12,0,0,0-12-12H56A12,12,0,0,0,44,80V192a12,12,0,0,0,12,12H200a12,12,0,0,0,12-12Zm-12,82a30,30,0,0,1-30,30H86a30,30,0,0,1,0-60h84A30,30,0,0,1,200,162Zm-80-6v12h16V156ZM86,168H96V156H86a6,6,0,0,0,0,12Zm90-6a6,6,0,0,0-6-6H160v12h10A6,6,0,0,0,176,162Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,56H56A24,24,0,0,0,32,80V192a24,24,0,0,0,24,24H200a24,24,0,0,0,24-24V80A24,24,0,0,0,200,56ZM164,184H92a20,20,0,0,1,0-40h72a20,20,0,0,1,0,40Z",opacity:"0.2"}),D.createElement("path",{d:"M200,48H136V16a8,8,0,0,0-16,0V48H56A32,32,0,0,0,24,80V192a32,32,0,0,0,32,32H200a32,32,0,0,0,32-32V80A32,32,0,0,0,200,48Zm16,144a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V80A16,16,0,0,1,56,64H200a16,16,0,0,1,16,16ZM72,108a12,12,0,1,1,12,12A12,12,0,0,1,72,108Zm88,0a12,12,0,1,1,12,12A12,12,0,0,1,160,108Zm4,28H92a28,28,0,0,0,0,56h72a28,28,0,0,0,0-56Zm-24,16v24H116V152ZM80,164a12,12,0,0,1,12-12h8v24H92A12,12,0,0,1,80,164Zm84,12h-8V152h8a12,12,0,0,1,0,24Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,48H136V16a8,8,0,0,0-16,0V48H56A32,32,0,0,0,24,80V192a32,32,0,0,0,32,32H200a32,32,0,0,0,32-32V80A32,32,0,0,0,200,48ZM172,96a12,12,0,1,1-12,12A12,12,0,0,1,172,96ZM96,184H80a16,16,0,0,1,0-32H96ZM84,120a12,12,0,1,1,12-12A12,12,0,0,1,84,120Zm60,64H112V152h32Zm32,0H160V152h16a16,16,0,0,1,0,32Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,50H134V16a6,6,0,0,0-12,0V50H56A30,30,0,0,0,26,80V192a30,30,0,0,0,30,30H200a30,30,0,0,0,30-30V80A30,30,0,0,0,200,50Zm18,142a18,18,0,0,1-18,18H56a18,18,0,0,1-18-18V80A18,18,0,0,1,56,62H200a18,18,0,0,1,18,18ZM74,108a10,10,0,1,1,10,10A10,10,0,0,1,74,108Zm88,0a10,10,0,1,1,10,10A10,10,0,0,1,162,108Zm2,30H92a26,26,0,0,0,0,52h72a26,26,0,0,0,0-52Zm-22,12v28H114V150ZM78,164a14,14,0,0,1,14-14h10v28H92A14,14,0,0,1,78,164Zm86,14H154V150h10a14,14,0,0,1,0,28Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,48H136V16a8,8,0,0,0-16,0V48H56A32,32,0,0,0,24,80V192a32,32,0,0,0,32,32H200a32,32,0,0,0,32-32V80A32,32,0,0,0,200,48Zm16,144a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V80A16,16,0,0,1,56,64H200a16,16,0,0,1,16,16Zm-52-56H92a28,28,0,0,0,0,56h72a28,28,0,0,0,0-56Zm-24,16v24H116V152ZM80,164a12,12,0,0,1,12-12h8v24H92A12,12,0,0,1,80,164Zm84,12h-8V152h8a12,12,0,0,1,0,24ZM72,108a12,12,0,1,1,12,12A12,12,0,0,1,72,108Zm88,0a12,12,0,1,1,12,12A12,12,0,0,1,160,108Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,52H132V16a4,4,0,0,0-8,0V52H56A28,28,0,0,0,28,80V192a28,28,0,0,0,28,28H200a28,28,0,0,0,28-28V80A28,28,0,0,0,200,52Zm20,140a20,20,0,0,1-20,20H56a20,20,0,0,1-20-20V80A20,20,0,0,1,56,60H200a20,20,0,0,1,20,20ZM76,108a8,8,0,1,1,8,8A8,8,0,0,1,76,108Zm88,0a8,8,0,1,1,8,8A8,8,0,0,1,164,108Zm0,32H92a24,24,0,0,0,0,48h72a24,24,0,0,0,0-48Zm-20,8v32H112V148ZM76,164a16,16,0,0,1,16-16h12v32H92A16,16,0,0,1,76,164Zm88,16H152V148h12a16,16,0,0,1,0,32Z"}))]]),A2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M227.85,46.89a20,20,0,0,0-18.74-18.74c-13.13-.77-46.65.42-74.48,28.24L131,60H74.36a19.83,19.83,0,0,0-14.14,5.86L25.87,100.19a20,20,0,0,0,11.35,33.95l37.14,5.18,42.32,42.32,5.19,37.18A19.88,19.88,0,0,0,135.34,235a20.13,20.13,0,0,0,6.37,1,19.9,19.9,0,0,0,14.1-5.87l34.34-34.35A19.85,19.85,0,0,0,196,181.64V125l3.6-3.59C227.43,93.54,228.62,60,227.85,46.89ZM76,84h31L75.75,115.28l-27.23-3.8ZM151.6,73.37A72.27,72.27,0,0,1,204,52a72.17,72.17,0,0,1-21.38,52.41L128,159,97,128ZM172,180l-27.49,27.49-3.8-27.23L172,149Zm-72,22c-8.71,11.85-26.19,26-60,26a12,12,0,0,1-12-12c0-33.84,14.12-51.32,26-60A12,12,0,1,1,68.18,175.3C62.3,179.63,55.51,187.8,53,203c15.21-2.51,23.37-9.3,27.7-15.18A12,12,0,1,1,100,202Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M184,120v61.65a8,8,0,0,1-2.34,5.65l-34.35,34.35a8,8,0,0,1-13.57-4.53L128,176ZM136,72H74.35a8,8,0,0,0-5.65,2.34L34.35,108.69a8,8,0,0,0,4.53,13.57L80,128ZM40,216c37.65,0,50.69-19.69,54.56-28.18L68.18,161.44C59.69,165.31,40,178.35,40,216Z",opacity:"0.2"}),D.createElement("path",{d:"M223.85,47.12a16,16,0,0,0-15-15c-12.58-.75-44.73.4-71.41,27.07L132.69,64H74.36A15.91,15.91,0,0,0,63,68.68L28.7,103a16,16,0,0,0,9.07,27.16l38.47,5.37,44.21,44.21,5.37,38.49a15.94,15.94,0,0,0,10.78,12.92,16.11,16.11,0,0,0,5.1.83A15.91,15.91,0,0,0,153,227.3L187.32,193A15.91,15.91,0,0,0,192,181.64V123.31l4.77-4.77C223.45,91.86,224.6,59.71,223.85,47.12ZM74.36,80h42.33L77.16,119.52,40,114.34Zm74.41-9.45a76.65,76.65,0,0,1,59.11-22.47,76.46,76.46,0,0,1-22.42,59.16L128,164.68,91.32,128ZM176,181.64,141.67,216l-5.19-37.17L176,139.31Zm-74.16,9.5C97.34,201,82.29,224,40,224a8,8,0,0,1-8-8c0-42.29,23-57.34,32.86-61.85a8,8,0,0,1,6.64,14.56c-6.43,2.93-20.62,12.36-23.12,38.91,26.55-2.5,36-16.69,38.91-23.12a8,8,0,1,1,14.56,6.64Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M101.85,191.14C97.34,201,82.29,224,40,224a8,8,0,0,1-8-8c0-42.29,23-57.34,32.86-61.85a8,8,0,0,1,6.64,14.56c-6.43,2.93-20.62,12.36-23.12,38.91,26.55-2.5,36-16.69,38.91-23.12a8,8,0,1,1,14.56,6.64Zm122-144a16,16,0,0,0-15-15c-12.58-.75-44.73.4-71.4,27.07h0L88,108.7A8,8,0,0,1,76.67,97.39l26.56-26.57A4,4,0,0,0,100.41,64H74.35A15.9,15.9,0,0,0,63,68.68L28.7,103a16,16,0,0,0,9.07,27.16l38.47,5.37,44.21,44.21,5.37,38.49a15.94,15.94,0,0,0,10.78,12.92,16.11,16.11,0,0,0,5.1.83A15.91,15.91,0,0,0,153,227.3L187.32,193A16,16,0,0,0,192,181.65V155.59a4,4,0,0,0-6.83-2.82l-26.57,26.56a8,8,0,0,1-11.71-.42,8.2,8.2,0,0,1,.6-11.1l49.27-49.27h0C223.45,91.86,224.6,59.71,223.85,47.12Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M221.86,47.24a14,14,0,0,0-13.11-13.1c-12.31-.73-43.77.39-69.88,26.5L133.52,66H74.35a13.9,13.9,0,0,0-9.89,4.1L30.11,104.44a14,14,0,0,0,7.94,23.76l39.13,5.46,45.16,45.16L127.8,218a14,14,0,0,0,23.76,7.92l34.35-34.35a13.91,13.91,0,0,0,4.1-9.89V122.48l5.35-5.35h0C221.46,91,222.59,59.56,221.86,47.24ZM38.11,115a2,2,0,0,1,.49-2L72.94,78.58A2,2,0,0,1,74.35,78h47.17L77.87,121.64l-38.14-5.32A1.93,1.93,0,0,1,38.11,115ZM178,181.65a2,2,0,0,1-.59,1.41L143.08,217.4a2,2,0,0,1-3.4-1.11l-5.32-38.16L178,134.48Zm8.87-73h0L128,167.51,88.49,128l58.87-58.88a78.47,78.47,0,0,1,60.69-23A2,2,0,0,1,209.88,48,78.47,78.47,0,0,1,186.88,108.64ZM100,190.31C95.68,199.84,81.13,222,40,222a6,6,0,0,1-6-6c0-41.13,22.16-55.68,31.69-60a6,6,0,1,1,5,10.92c-7,3.17-22.53,13.52-24.47,42.91,29.39-1.94,39.74-17.52,42.91-24.47a6,6,0,1,1,10.92,5Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M223.85,47.12a16,16,0,0,0-15-15c-12.58-.75-44.73.4-71.41,27.07L132.69,64H74.36A15.91,15.91,0,0,0,63,68.68L28.7,103a16,16,0,0,0,9.07,27.16l38.47,5.37,44.21,44.21,5.37,38.49a15.94,15.94,0,0,0,10.78,12.92,16.11,16.11,0,0,0,5.1.83A15.91,15.91,0,0,0,153,227.3L187.32,193A15.91,15.91,0,0,0,192,181.64V123.31l4.77-4.77C223.45,91.86,224.6,59.71,223.85,47.12ZM74.36,80h42.33L77.16,119.52,40,114.34Zm74.41-9.45a76.65,76.65,0,0,1,59.11-22.47,76.46,76.46,0,0,1-22.42,59.16L128,164.68,91.32,128ZM176,181.64,141.67,216l-5.19-37.17L176,139.31Zm-74.16,9.5C97.34,201,82.29,224,40,224a8,8,0,0,1-8-8c0-42.29,23-57.34,32.86-61.85a8,8,0,0,1,6.64,14.56c-6.43,2.93-20.62,12.36-23.12,38.91,26.55-2.5,36-16.69,38.91-23.12a8,8,0,1,1,14.56,6.64Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M219.86,47.36a12,12,0,0,0-11.22-11.22c-12-.71-42.82.38-68.35,25.91L134.35,68h-60a11.9,11.9,0,0,0-8.48,3.52L31.52,105.85a12,12,0,0,0,6.81,20.37l39.79,5.55,46.11,46.11,5.55,39.81a12,12,0,0,0,20.37,6.79l34.34-34.35a11.9,11.9,0,0,0,3.52-8.48v-60l5.94-5.94C219.48,90.18,220.57,59.41,219.86,47.36ZM36.21,115.6a3.94,3.94,0,0,1,1-4.09L71.53,77.17A4,4,0,0,1,74.35,76h52L78.58,123.76,39.44,118.3A3.94,3.94,0,0,1,36.21,115.6ZM180,181.65a4,4,0,0,1-1.17,2.83l-34.35,34.34a4,4,0,0,1-6.79-2.25l-5.46-39.15L180,129.65Zm-52-11.31L85.66,128l60.28-60.29c23.24-23.24,51.25-24.23,62.22-23.58a3.93,3.93,0,0,1,3.71,3.71c.65,11-.35,39-23.58,62.22ZM98.21,189.48C94,198.66,80,220,40,220a4,4,0,0,1-4-4c0-40,21.34-54,30.52-58.21a4,4,0,0,1,3.32,7.28c-7.46,3.41-24.43,14.66-25.76,46.85,32.19-1.33,43.44-18.3,46.85-25.76a4,4,0,1,1,7.28,3.32Z"}))]]),R2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,136H48a20,20,0,0,0-20,20v36a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V156A20,20,0,0,0,208,136Zm-4,52H52V160H204Zm4-144H48A20,20,0,0,0,28,64v36a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V64A20,20,0,0,0,208,44Zm-4,52H52V68H204Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,152v40a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V152a8,8,0,0,1,8-8H208A8,8,0,0,1,216,152Zm-8-96H48a8,8,0,0,0-8,8v40a8,8,0,0,0,8,8H208a8,8,0,0,0,8-8V64A8,8,0,0,0,208,56Z",opacity:"0.2"}),D.createElement("path",{d:"M208,136H48a16,16,0,0,0-16,16v40a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V152A16,16,0,0,0,208,136Zm0,56H48V152H208v40Zm0-144H48A16,16,0,0,0,32,64v40a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V64A16,16,0,0,0,208,48Zm0,56H48V64H208v40Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,152v40a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V152a16,16,0,0,1,16-16H208A16,16,0,0,1,224,152ZM208,48H48A16,16,0,0,0,32,64v40a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V64A16,16,0,0,0,208,48Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,138H48a14,14,0,0,0-14,14v40a14,14,0,0,0,14,14H208a14,14,0,0,0,14-14V152A14,14,0,0,0,208,138Zm2,54a2,2,0,0,1-2,2H48a2,2,0,0,1-2-2V152a2,2,0,0,1,2-2H208a2,2,0,0,1,2,2ZM208,50H48A14,14,0,0,0,34,64v40a14,14,0,0,0,14,14H208a14,14,0,0,0,14-14V64A14,14,0,0,0,208,50Zm2,54a2,2,0,0,1-2,2H48a2,2,0,0,1-2-2V64a2,2,0,0,1,2-2H208a2,2,0,0,1,2,2Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,136H48a16,16,0,0,0-16,16v40a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V152A16,16,0,0,0,208,136Zm0,56H48V152H208v40Zm0-144H48A16,16,0,0,0,32,64v40a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V64A16,16,0,0,0,208,48Zm0,56H48V64H208v40Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,140H48a12,12,0,0,0-12,12v40a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V152A12,12,0,0,0,208,140Zm4,52a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V152a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4ZM208,52H48A12,12,0,0,0,36,64v40a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V64A12,12,0,0,0,208,52Zm4,52a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V64a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Z"}))]]),M2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M238.78,183.79,98.28,87.65A40.18,40.18,0,0,0,100,76a40,40,0,1,0-15.29,31.45l30,20.56-30,20.56a40,40,0,1,0,3.57,59.74h0A39.73,39.73,0,0,0,100,180a40.18,40.18,0,0,0-1.72-11.66L136,142.54l89.22,61.06a12,12,0,0,0,13.56-19.81ZM71.31,191.33h0A16,16,0,1,1,76,180,16,16,0,0,1,71.31,191.33ZM48.69,87.3a16,16,0,1,1,22.62,0h0A16,16,0,0,1,48.69,87.3Zm112.82,23.24a12,12,0,0,1,3.13-16.68L225.22,52.4a12,12,0,0,1,13.56,19.81l-60.59,41.46a12,12,0,0,1-16.68-3.13Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M40.2,95.8a28,28,0,1,1,39.6,0A28,28,0,0,1,40.2,95.8Zm0,64.4a28,28,0,1,0,39.6,0A28,28,0,0,0,40.2,160.2Z",opacity:"0.2"}),D.createElement("path",{d:"M157.73,113.13A8,8,0,0,1,159.82,102L227.48,55.7a8,8,0,0,1,9,13.21l-67.67,46.3a7.92,7.92,0,0,1-4.51,1.4A8,8,0,0,1,157.73,113.13Zm80.87,85.09a8,8,0,0,1-11.12,2.08L136,137.7,93.49,166.78a36,36,0,1,1-9-13.19L121.83,128,84.44,102.41a35.86,35.86,0,1,1,9-13.19l143,97.87A8,8,0,0,1,238.6,198.22ZM80,180a20,20,0,1,0-5.86,14.14A19.85,19.85,0,0,0,80,180ZM74.14,90.13a20,20,0,1,0-28.28,0A19.85,19.85,0,0,0,74.14,90.13Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M236.52,187.09l-143-97.87a36,36,0,1,0-14.38,17.27l21.39,21.69L79.15,149.54l0,0a35.91,35.91,0,1,0,14.38,17.27l26.91-18.41L170,198.64a32.26,32.26,0,0,0,22.7,9.37,31.52,31.52,0,0,0,4.11-.27l.28,0,36.27-6.11a8,8,0,0,0,3.19-14.5Zm-162.38-97A20,20,0,1,1,80,76,20,20,0,0,1,74.14,90.13Zm0,104A20,20,0,1,1,80,180,20,20,0,0,1,74.14,194.15Zm61-101.5L169.94,57.4a32.19,32.19,0,0,1,26.84-9.14l.28,0,36,6.07a8.21,8.21,0,0,1,6.09,4.42,8,8,0,0,1-2.67,10.12l-69.93,47.85a4,4,0,0,1-4.51,0l-26.31-18A4,4,0,0,1,135.18,92.65Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M159.38,112a6,6,0,0,1,1.57-8.34l67.66-46.31a6,6,0,0,1,6.78,9.91l-67.67,46.3a6,6,0,0,1-8.34-1.56ZM237,197.09a6,6,0,0,1-8.34,1.56L136,135.27,91,166.06A34,34,0,1,1,84,156a1.8,1.8,0,0,0,.19.2L125.37,128,84.23,99.84,84,100a34,34,0,1,1,7-10.1l144.38,98.8A6,6,0,0,1,237,197.09ZM75.56,91.55a22,22,0,1,0-31.12,0,21.88,21.88,0,0,0,31.12,0ZM82,180a22,22,0,1,0-6.44,15.56h0A21.88,21.88,0,0,0,82,180Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M157.73,113.13A8,8,0,0,1,159.82,102L227.48,55.7a8,8,0,0,1,9,13.21l-67.67,46.3a7.92,7.92,0,0,1-4.51,1.4A8,8,0,0,1,157.73,113.13Zm80.87,85.09a8,8,0,0,1-11.12,2.08L136,137.7,93.49,166.78a36,36,0,1,1-9-13.19L121.83,128,84.44,102.41a35.86,35.86,0,1,1,9-13.19l143,97.87A8,8,0,0,1,238.6,198.22ZM80,180a20,20,0,1,0-5.86,14.14A19.85,19.85,0,0,0,80,180ZM74.14,90.13a20,20,0,1,0-28.28,0A19.85,19.85,0,0,0,74.14,90.13Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M161,110.87a4,4,0,0,1,1.05-5.56L229.74,59a4,4,0,0,1,4.52,6.61l-67.67,46.3a4,4,0,0,1-5.56-1ZM235.3,196a4,4,0,0,1-5.56,1L136,132.85,88.47,165.38a32,32,0,1,1-5.84-8c.45.45.89.92,1.31,1.4l45-30.78-45-30.78c-.42.48-.86,1-1.31,1.4a31.86,31.86,0,1,1,5.84-8l49.69,34h0l96.09,65.76A4,4,0,0,1,235.3,196ZM77,93a24.42,24.42,0,0,0,2.82-3.38s0,0,0,0l0,0A24,24,0,1,0,77,93ZM84,180a23.75,23.75,0,0,0-4.15-13.49l0-.06,0,0A24.5,24.5,0,0,0,77,163,24,24,0,1,0,77,197h0A23.85,23.85,0,0,0,84,180Z"}))]]),O2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,36H48A20,20,0,0,0,28,56v56c0,54.29,26.32,87.22,48.4,105.29,23.71,19.39,47.44,26,48.44,26.29a12.1,12.1,0,0,0,6.32,0c1-.28,24.73-6.9,48.44-26.29,22.08-18.07,48.4-51,48.4-105.29V56A20,20,0,0,0,208,36Zm-4,76c0,35.71-13.09,64.69-38.91,86.15A126.28,126.28,0,0,1,128,219.38a126.14,126.14,0,0,1-37.09-21.23C65.09,176.69,52,147.71,52,112V60H204ZM79.51,144.49a12,12,0,1,1,17-17L112,143l47.51-47.52a12,12,0,0,1,17,17l-56,56a12,12,0,0,1-17,0Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,56v56c0,96-88,120-88,120S40,208,40,112V56a8,8,0,0,1,8-8H208A8,8,0,0,1,216,56Z",opacity:"0.2"}),D.createElement("path",{d:"M208,40H48A16,16,0,0,0,32,56v56c0,52.72,25.52,84.67,46.93,102.19,23.06,18.86,46,25.26,47,25.53a8,8,0,0,0,4.2,0c1-.27,23.91-6.67,47-25.53C198.48,196.67,224,164.72,224,112V56A16,16,0,0,0,208,40Zm0,72c0,37.07-13.66,67.16-40.6,89.42A129.3,129.3,0,0,1,128,223.62a128.25,128.25,0,0,1-38.92-21.81C61.82,179.51,48,149.3,48,112l0-56,160,0ZM82.34,141.66a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32l-56,56a8,8,0,0,1-11.32,0Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,40H48A16,16,0,0,0,32,56v56c0,52.72,25.52,84.67,46.93,102.19,23.06,18.86,46,25.26,47,25.53a8,8,0,0,0,4.2,0c1-.27,23.91-6.67,47-25.53C198.48,196.67,224,164.72,224,112V56A16,16,0,0,0,208,40Zm-34.32,69.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,42H48A14,14,0,0,0,34,56v56c0,51.94,25.12,83.4,46.2,100.64,22.73,18.6,45.27,24.89,46.22,25.15a6,6,0,0,0,3.16,0c.95-.26,23.49-6.55,46.22-25.15C196.88,195.4,222,163.94,222,112V56A14,14,0,0,0,208,42Zm2,70c0,37.76-13.94,68.39-41.44,91.06A131.17,131.17,0,0,1,128,225.72a130.94,130.94,0,0,1-40.56-22.66C59.94,180.39,46,149.76,46,112V56a2,2,0,0,1,2-2H208a2,2,0,0,1,2,2ZM172.24,99.76a6,6,0,0,1,0,8.48l-56,56a6,6,0,0,1-8.48,0l-24-24a6,6,0,0,1,8.48-8.48L112,151.51l51.76-51.75A6,6,0,0,1,172.24,99.76Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,40H48A16,16,0,0,0,32,56v56c0,52.72,25.52,84.67,46.93,102.19,23.06,18.86,46,25.26,47,25.53a8,8,0,0,0,4.2,0c1-.27,23.91-6.67,47-25.53C198.48,196.67,224,164.72,224,112V56A16,16,0,0,0,208,40Zm0,72c0,37.07-13.66,67.16-40.6,89.42A129.3,129.3,0,0,1,128,223.62a128.25,128.25,0,0,1-38.92-21.81C61.82,179.51,48,149.3,48,112l0-56,160,0ZM82.34,141.66a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32l-56,56a8,8,0,0,1-11.32,0Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,44H48A12,12,0,0,0,36,56v56c0,51.16,24.73,82.12,45.47,99.1,22.4,18.32,44.55,24.5,45.48,24.76a4,4,0,0,0,2.1,0c.93-.26,23.08-6.44,45.48-24.76,20.74-17,45.47-47.94,45.47-99.1V56A12,12,0,0,0,208,44Zm4,68c0,38.44-14.23,69.63-42.29,92.71A132.45,132.45,0,0,1,128,227.82a132.23,132.23,0,0,1-41.71-23.11C58.23,181.63,44,150.44,44,112V56a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-41.17-10.83a4,4,0,0,1,0,5.66l-56,56a4,4,0,0,1-5.66,0l-24-24a4,4,0,0,1,5.66-5.66L112,154.34l53.17-53.17A4,4,0,0,1,170.83,101.17Z"}))]]),N2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M116,132V96a12,12,0,0,1,24,0v36a12,12,0,0,1-24,0Zm12,56a16,16,0,1,0-16-16A16,16,0,0,0,128,188ZM228,56v56c0,54.29-26.32,87.22-48.4,105.29-23.71,19.39-47.44,26-48.44,26.29a12.1,12.1,0,0,1-6.32,0c-1-.28-24.73-6.9-48.44-26.29C54.32,199.22,28,166.29,28,112V56A20,20,0,0,1,48,36H208A20,20,0,0,1,228,56Zm-24,4H52v52c0,35.71,13.09,64.69,38.91,86.15A126.14,126.14,0,0,0,128,219.38a126.28,126.28,0,0,0,37.09-21.23C190.91,176.69,204,147.71,204,112Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,56v56c0,96-88,120-88,120S40,208,40,112V56a8,8,0,0,1,8-8H208A8,8,0,0,1,216,56Z",opacity:"0.2"}),D.createElement("path",{d:"M120,136V96a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm8,48a12,12,0,1,0-12-12A12,12,0,0,0,128,184ZM224,56v56c0,52.72-25.52,84.67-46.93,102.19-23.06,18.86-46,25.27-47,25.53a8,8,0,0,1-4.2,0c-1-.26-23.91-6.67-47-25.53C57.52,196.67,32,164.72,32,112V56A16,16,0,0,1,48,40H208A16,16,0,0,1,224,56Zm-16,0L48,56l0,56c0,37.3,13.82,67.51,41.07,89.81A128.25,128.25,0,0,0,128,223.62a129.3,129.3,0,0,0,39.41-22.2C194.34,179.16,208,149.07,208,112Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,40H48A16,16,0,0,0,32,56v56c0,52.72,25.52,84.67,46.93,102.19,23.06,18.86,46,25.27,47,25.53a8,8,0,0,0,4.2,0c1-.26,23.91-6.67,47-25.53C198.48,196.67,224,164.72,224,112V56A16,16,0,0,0,208,40ZM120,96a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm8,88a12,12,0,1,1,12-12A12,12,0,0,1,128,184Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M122,136V96a6,6,0,0,1,12,0v40a6,6,0,0,1-12,0Zm6,26a10,10,0,1,0,10,10A10,10,0,0,0,128,162ZM222,56v56c0,51.94-25.12,83.4-46.2,100.64-22.73,18.6-45.27,24.89-46.22,25.15a6,6,0,0,1-3.16,0c-1-.26-23.49-6.55-46.22-25.15C59.12,195.4,34,163.94,34,112V56A14,14,0,0,1,48,42H208A14,14,0,0,1,222,56Zm-12,0a2,2,0,0,0-2-2H48a2,2,0,0,0-2,2v56c0,37.75,13.94,68.39,41.44,91.06A130.94,130.94,0,0,0,128,225.72a131.17,131.17,0,0,0,40.56-22.66C196.06,180.39,210,149.75,210,112Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M120,136V96a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm8,48a12,12,0,1,0-12-12A12,12,0,0,0,128,184ZM224,56v56c0,52.72-25.52,84.67-46.93,102.19-23.06,18.86-46,25.27-47,25.53a8,8,0,0,1-4.2,0c-1-.26-23.91-6.67-47-25.53C57.52,196.67,32,164.72,32,112V56A16,16,0,0,1,48,40H208A16,16,0,0,1,224,56Zm-16,0L48,56l0,56c0,37.3,13.82,67.51,41.07,89.81A128.25,128.25,0,0,0,128,223.62a129.3,129.3,0,0,0,39.41-22.2C194.34,179.16,208,149.07,208,112Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M124,136V96a4,4,0,0,1,8,0v40a4,4,0,0,1-8,0Zm4,28a8,8,0,1,0,8,8A8,8,0,0,0,128,164ZM220,56v56c0,51.16-24.73,82.12-45.47,99.1-22.4,18.32-44.55,24.5-45.48,24.76a4,4,0,0,1-2.1,0c-.93-.26-23.08-6.44-45.48-24.76C60.73,194.12,36,163.16,36,112V56A12,12,0,0,1,48,44H208A12,12,0,0,1,220,56Zm-8,0a4,4,0,0,0-4-4H48a4,4,0,0,0-4,4v56c0,38.44,14.23,69.63,42.29,92.71A132.23,132.23,0,0,0,128,227.82a132.45,132.45,0,0,0,41.71-23.11C197.77,181.63,212,150.44,212,112Z"}))]]),P2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M140,32V64a12,12,0,0,1-24,0V32a12,12,0,0,1,24,0Zm33.25,62.75a12,12,0,0,0,8.49-3.52L204.37,68.6a12,12,0,0,0-17-17L164.77,74.26a12,12,0,0,0,8.48,20.49ZM224,116H192a12,12,0,0,0,0,24h32a12,12,0,0,0,0-24Zm-42.26,48.77a12,12,0,1,0-17,17l22.63,22.63a12,12,0,0,0,17-17ZM128,180a12,12,0,0,0-12,12v32a12,12,0,0,0,24,0V192A12,12,0,0,0,128,180ZM74.26,164.77,51.63,187.4a12,12,0,0,0,17,17l22.63-22.63a12,12,0,1,0-17-17ZM76,128a12,12,0,0,0-12-12H32a12,12,0,0,0,0,24H64A12,12,0,0,0,76,128ZM68.6,51.63a12,12,0,1,0-17,17L74.26,91.23a12,12,0,0,0,17-17Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z",opacity:"0.2"}),D.createElement("path",{d:"M136,32V64a8,8,0,0,1-16,0V32a8,8,0,0,1,16,0Zm37.25,58.75a8,8,0,0,0,5.66-2.35l22.63-22.62a8,8,0,0,0-11.32-11.32L167.6,77.09a8,8,0,0,0,5.65,13.66ZM224,120H192a8,8,0,0,0,0,16h32a8,8,0,0,0,0-16Zm-45.09,47.6a8,8,0,0,0-11.31,11.31l22.62,22.63a8,8,0,0,0,11.32-11.32ZM128,184a8,8,0,0,0-8,8v32a8,8,0,0,0,16,0V192A8,8,0,0,0,128,184ZM77.09,167.6,54.46,190.22a8,8,0,0,0,11.32,11.32L88.4,178.91A8,8,0,0,0,77.09,167.6ZM72,128a8,8,0,0,0-8-8H32a8,8,0,0,0,0,16H64A8,8,0,0,0,72,128ZM65.78,54.46A8,8,0,0,0,54.46,65.78L77.09,88.4A8,8,0,0,0,88.4,77.09Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm33.94,58.75,17-17a8,8,0,0,1,11.32,11.32l-17,17a8,8,0,0,1-11.31-11.31ZM48,136a8,8,0,0,1,0-16H72a8,8,0,0,1,0,16Zm46.06,37.25-17,17a8,8,0,0,1-11.32-11.32l17-17a8,8,0,0,1,11.31,11.31Zm0-79.19a8,8,0,0,1-11.31,0l-17-17A8,8,0,0,1,77.09,65.77l17,17A8,8,0,0,1,94.06,94.06ZM136,208a8,8,0,0,1-16,0V184a8,8,0,0,1,16,0Zm0-136a8,8,0,0,1-16,0V48a8,8,0,0,1,16,0Zm54.23,118.23a8,8,0,0,1-11.32,0l-17-17a8,8,0,0,1,11.31-11.31l17,17A8,8,0,0,1,190.23,190.23ZM208,136H184a8,8,0,0,1,0-16h24a8,8,0,0,1,0,16Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M134,32V64a6,6,0,0,1-12,0V32a6,6,0,0,1,12,0Zm39.25,56.75A6,6,0,0,0,177.5,87l22.62-22.63a6,6,0,0,0-8.48-8.48L169,78.5a6,6,0,0,0,4.24,10.25ZM224,122H192a6,6,0,0,0,0,12h32a6,6,0,0,0,0-12Zm-46.5,47A6,6,0,0,0,169,177.5l22.63,22.62a6,6,0,0,0,8.48-8.48ZM128,186a6,6,0,0,0-6,6v32a6,6,0,0,0,12,0V192A6,6,0,0,0,128,186ZM78.5,169,55.88,191.64a6,6,0,1,0,8.48,8.48L87,177.5A6,6,0,1,0,78.5,169ZM70,128a6,6,0,0,0-6-6H32a6,6,0,0,0,0,12H64A6,6,0,0,0,70,128ZM64.36,55.88a6,6,0,0,0-8.48,8.48L78.5,87A6,6,0,1,0,87,78.5Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M136,32V64a8,8,0,0,1-16,0V32a8,8,0,0,1,16,0Zm37.25,58.75a8,8,0,0,0,5.66-2.35l22.63-22.62a8,8,0,0,0-11.32-11.32L167.6,77.09a8,8,0,0,0,5.65,13.66ZM224,120H192a8,8,0,0,0,0,16h32a8,8,0,0,0,0-16Zm-45.09,47.6a8,8,0,0,0-11.31,11.31l22.62,22.63a8,8,0,0,0,11.32-11.32ZM128,184a8,8,0,0,0-8,8v32a8,8,0,0,0,16,0V192A8,8,0,0,0,128,184ZM77.09,167.6,54.46,190.22a8,8,0,0,0,11.32,11.32L88.4,178.91A8,8,0,0,0,77.09,167.6ZM72,128a8,8,0,0,0-8-8H32a8,8,0,0,0,0,16H64A8,8,0,0,0,72,128ZM65.78,54.46A8,8,0,0,0,54.46,65.78L77.09,88.4A8,8,0,0,0,88.4,77.09Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M132,32V64a4,4,0,0,1-8,0V32a4,4,0,0,1,8,0Zm41.25,54.75a4,4,0,0,0,2.83-1.18L198.71,63a4,4,0,0,0-5.66-5.66L170.43,79.92a4,4,0,0,0,2.82,6.83ZM224,124H192a4,4,0,0,0,0,8h32a4,4,0,0,0,0-8Zm-47.92,46.43a4,4,0,1,0-5.65,5.65l22.62,22.63a4,4,0,0,0,5.66-5.66ZM128,188a4,4,0,0,0-4,4v32a4,4,0,0,0,8,0V192A4,4,0,0,0,128,188ZM79.92,170.43,57.29,193.05A4,4,0,0,0,63,198.71l22.62-22.63a4,4,0,1,0-5.65-5.65ZM68,128a4,4,0,0,0-4-4H32a4,4,0,0,0,0,8H64A4,4,0,0,0,68,128ZM63,57.29A4,4,0,0,0,57.29,63L79.92,85.57a4,4,0,1,0,5.65-5.65Z"}))]]),F2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,36H56A20,20,0,0,0,36,56V200a20,20,0,0,0,20,20H200a20,20,0,0,0,20-20V56A20,20,0,0,0,200,36Zm-4,160H60V60H196Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,56V200a8,8,0,0,1-8,8H56a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8H200A8,8,0,0,1,208,56Z",opacity:"0.2"}),D.createElement("path",{d:"M200,40H56A16,16,0,0,0,40,56V200a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,160H56V56H200V200Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,56V200a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V56A16,16,0,0,1,56,40H200A16,16,0,0,1,216,56Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,42H56A14,14,0,0,0,42,56V200a14,14,0,0,0,14,14H200a14,14,0,0,0,14-14V56A14,14,0,0,0,200,42Zm2,158a2,2,0,0,1-2,2H56a2,2,0,0,1-2-2V56a2,2,0,0,1,2-2H200a2,2,0,0,1,2,2Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,40H56A16,16,0,0,0,40,56V200a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,160H56V56H200V200Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,44H56A12,12,0,0,0,44,56V200a12,12,0,0,0,12,12H200a12,12,0,0,0,12-12V56A12,12,0,0,0,200,44Zm4,156a4,4,0,0,1-4,4H56a4,4,0,0,1-4-4V56a4,4,0,0,1,4-4H200a4,4,0,0,1,4,4Z"}))]]),j2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M246.15,133.18,146.83,33.86A19.85,19.85,0,0,0,132.69,28H40A12,12,0,0,0,28,40v92.69a19.85,19.85,0,0,0,5.86,14.14l99.32,99.32a20,20,0,0,0,28.28,0l84.69-84.69A20,20,0,0,0,246.15,133.18Zm-98.83,93.17L52,131V52h79l95.32,95.32ZM104,88A16,16,0,1,1,88,72,16,16,0,0,1,104,88Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M237.66,153,153,237.66a8,8,0,0,1-11.31,0L42.34,138.34A8,8,0,0,1,40,132.69V40h92.69a8,8,0,0,1,5.65,2.34l99.32,99.32A8,8,0,0,1,237.66,153Z",opacity:"0.2"}),D.createElement("path",{d:"M243.31,136,144,36.69A15.86,15.86,0,0,0,132.69,32H40a8,8,0,0,0-8,8v92.69A15.86,15.86,0,0,0,36.69,144L136,243.31a16,16,0,0,0,22.63,0l84.68-84.68a16,16,0,0,0,0-22.63Zm-96,96L48,132.69V48h84.69L232,147.31ZM96,84A12,12,0,1,1,84,72,12,12,0,0,1,96,84Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M243.31,136,144,36.69A15.86,15.86,0,0,0,132.69,32H40a8,8,0,0,0-8,8v92.69A15.86,15.86,0,0,0,36.69,144L136,243.31a16,16,0,0,0,22.63,0l84.68-84.68a16,16,0,0,0,0-22.63ZM84,96A12,12,0,1,1,96,84,12,12,0,0,1,84,96Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M241.91,137.42,142.59,38.1a13.94,13.94,0,0,0-9.9-4.1H40a6,6,0,0,0-6,6v92.69a13.94,13.94,0,0,0,4.1,9.9l99.32,99.32a14,14,0,0,0,19.8,0l84.69-84.69A14,14,0,0,0,241.91,137.42Zm-8.49,11.31-84.69,84.69a2,2,0,0,1-2.83,0L46.59,134.1a2,2,0,0,1-.59-1.41V46h86.69a2,2,0,0,1,1.41.59l99.32,99.31A2,2,0,0,1,233.42,148.73ZM94,84A10,10,0,1,1,84,74,10,10,0,0,1,94,84Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M243.31,136,144,36.69A15.86,15.86,0,0,0,132.69,32H40a8,8,0,0,0-8,8v92.69A15.86,15.86,0,0,0,36.69,144L136,243.31a16,16,0,0,0,22.63,0l84.68-84.68a16,16,0,0,0,0-22.63Zm-96,96L48,132.69V48h84.69L232,147.31ZM96,84A12,12,0,1,1,84,72,12,12,0,0,1,96,84Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240.49,138.83,141.17,39.51A11.93,11.93,0,0,0,132.69,36H40a4,4,0,0,0-4,4v92.69a11.93,11.93,0,0,0,3.51,8.48l99.32,99.32a12,12,0,0,0,17,0l84.69-84.69a12,12,0,0,0,0-17Zm-5.66,11.31-84.69,84.69a4,4,0,0,1-5.65,0L45.17,135.51A4,4,0,0,1,44,132.69V44h88.69a4,4,0,0,1,2.82,1.17l99.32,99.32A4,4,0,0,1,234.83,150.14ZM92,84a8,8,0,1,1-8-8A8,8,0,0,1,92,84Z"}))]]),H2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,48H180V36A28,28,0,0,0,152,8H104A28,28,0,0,0,76,36V48H40a12,12,0,0,0,0,24h4V208a20,20,0,0,0,20,20H192a20,20,0,0,0,20-20V72h4a12,12,0,0,0,0-24ZM100,36a4,4,0,0,1,4-4h48a4,4,0,0,1,4,4V48H100Zm88,168H68V72H188ZM116,104v64a12,12,0,0,1-24,0V104a12,12,0,0,1,24,0Zm48,0v64a12,12,0,0,1-24,0V104a12,12,0,0,1,24,0Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M200,56V208a8,8,0,0,1-8,8H64a8,8,0,0,1-8-8V56Z",opacity:"0.2"}),D.createElement("path",{d:"M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM112,168a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm0-120H96V40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,50H174V40a22,22,0,0,0-22-22H104A22,22,0,0,0,82,40V50H40a6,6,0,0,0,0,12H50V208a14,14,0,0,0,14,14H192a14,14,0,0,0,14-14V62h10a6,6,0,0,0,0-12ZM94,40a10,10,0,0,1,10-10h48a10,10,0,0,1,10,10V50H94ZM194,208a2,2,0,0,1-2,2H64a2,2,0,0,1-2-2V62H194ZM110,104v64a6,6,0,0,1-12,0V104a6,6,0,0,1,12,0Zm48,0v64a6,6,0,0,1-12,0V104a6,6,0,0,1,12,0Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,52H172V40a20,20,0,0,0-20-20H104A20,20,0,0,0,84,40V52H40a4,4,0,0,0,0,8H52V208a12,12,0,0,0,12,12H192a12,12,0,0,0,12-12V60h12a4,4,0,0,0,0-8ZM92,40a12,12,0,0,1,12-12h48a12,12,0,0,1,12,12V52H92ZM196,208a4,4,0,0,1-4,4H64a4,4,0,0,1-4-4V60H196ZM108,104v64a4,4,0,0,1-8,0V104a4,4,0,0,1,8,0Zm48,0v64a4,4,0,0,1-8,0V104a4,4,0,0,1,8,0Z"}))]]),B2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M160,116h48a20,20,0,0,0,20-20V48a20,20,0,0,0-20-20H160a20,20,0,0,0-20,20V60H128a28,28,0,0,0-28,28v28H76v-4A20,20,0,0,0,56,92H24A20,20,0,0,0,4,112v32a20,20,0,0,0,20,20H56a20,20,0,0,0,20-20v-4h24v28a28,28,0,0,0,28,28h12v12a20,20,0,0,0,20,20h48a20,20,0,0,0,20-20V160a20,20,0,0,0-20-20H160a20,20,0,0,0-20,20v12H128a4,4,0,0,1-4-4V88a4,4,0,0,1,4-4h12V96A20,20,0,0,0,160,116ZM52,140H28V116H52Zm112,24h40v40H164Zm0-112h40V92H164Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M64,112v32a8,8,0,0,1-8,8H24a8,8,0,0,1-8-8V112a8,8,0,0,1,8-8H56A8,8,0,0,1,64,112ZM208,40H160a8,8,0,0,0-8,8V96a8,8,0,0,0,8,8h48a8,8,0,0,0,8-8V48A8,8,0,0,0,208,40Zm0,112H160a8,8,0,0,0-8,8v48a8,8,0,0,0,8,8h48a8,8,0,0,0,8-8V160A8,8,0,0,0,208,152Z",opacity:"0.2"}),D.createElement("path",{d:"M160,112h48a16,16,0,0,0,16-16V48a16,16,0,0,0-16-16H160a16,16,0,0,0-16,16V64H128a24,24,0,0,0-24,24v32H72v-8A16,16,0,0,0,56,96H24A16,16,0,0,0,8,112v32a16,16,0,0,0,16,16H56a16,16,0,0,0,16-16v-8h32v32a24,24,0,0,0,24,24h16v16a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V160a16,16,0,0,0-16-16H160a16,16,0,0,0-16,16v16H128a8,8,0,0,1-8-8V88a8,8,0,0,1,8-8h16V96A16,16,0,0,0,160,112ZM56,144H24V112H56v32Zm104,16h48v48H160Zm0-112h48V96H160Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M144,96V80H128a8,8,0,0,0-8,8v80a8,8,0,0,0,8,8h16V160a16,16,0,0,1,16-16h48a16,16,0,0,1,16,16v48a16,16,0,0,1-16,16H160a16,16,0,0,1-16-16V192H128a24,24,0,0,1-24-24V136H72v8a16,16,0,0,1-16,16H24A16,16,0,0,1,8,144V112A16,16,0,0,1,24,96H56a16,16,0,0,1,16,16v8h32V88a24,24,0,0,1,24-24h16V48a16,16,0,0,1,16-16h48a16,16,0,0,1,16,16V96a16,16,0,0,1-16,16H160A16,16,0,0,1,144,96Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M160,110h48a14,14,0,0,0,14-14V48a14,14,0,0,0-14-14H160a14,14,0,0,0-14,14V66H128a22,22,0,0,0-22,22v34H70V112A14,14,0,0,0,56,98H24a14,14,0,0,0-14,14v32a14,14,0,0,0,14,14H56a14,14,0,0,0,14-14V134h36v34a22,22,0,0,0,22,22h18v18a14,14,0,0,0,14,14h48a14,14,0,0,0,14-14V160a14,14,0,0,0-14-14H160a14,14,0,0,0-14,14v18H128a10,10,0,0,1-10-10V88a10,10,0,0,1,10-10h18V96A14,14,0,0,0,160,110ZM58,144a2,2,0,0,1-2,2H24a2,2,0,0,1-2-2V112a2,2,0,0,1,2-2H56a2,2,0,0,1,2,2Zm100,16a2,2,0,0,1,2-2h48a2,2,0,0,1,2,2v48a2,2,0,0,1-2,2H160a2,2,0,0,1-2-2Zm0-112a2,2,0,0,1,2-2h48a2,2,0,0,1,2,2V96a2,2,0,0,1-2,2H160a2,2,0,0,1-2-2Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M160,112h48a16,16,0,0,0,16-16V48a16,16,0,0,0-16-16H160a16,16,0,0,0-16,16V64H128a24,24,0,0,0-24,24v32H72v-8A16,16,0,0,0,56,96H24A16,16,0,0,0,8,112v32a16,16,0,0,0,16,16H56a16,16,0,0,0,16-16v-8h32v32a24,24,0,0,0,24,24h16v16a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V160a16,16,0,0,0-16-16H160a16,16,0,0,0-16,16v16H128a8,8,0,0,1-8-8V88a8,8,0,0,1,8-8h16V96A16,16,0,0,0,160,112ZM56,144H24V112H56v32Zm104,16h48v48H160Zm0-112h48V96H160Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M160,108h48a12,12,0,0,0,12-12V48a12,12,0,0,0-12-12H160a12,12,0,0,0-12,12V68H128a20,20,0,0,0-20,20v36H68V112a12,12,0,0,0-12-12H24a12,12,0,0,0-12,12v32a12,12,0,0,0,12,12H56a12,12,0,0,0,12-12V132h40v36a20,20,0,0,0,20,20h20v20a12,12,0,0,0,12,12h48a12,12,0,0,0,12-12V160a12,12,0,0,0-12-12H160a12,12,0,0,0-12,12v20H128a12,12,0,0,1-12-12V88a12,12,0,0,1,12-12h20V96A12,12,0,0,0,160,108ZM60,144a4,4,0,0,1-4,4H24a4,4,0,0,1-4-4V112a4,4,0,0,1,4-4H56a4,4,0,0,1,4,4Zm96,16a4,4,0,0,1,4-4h48a4,4,0,0,1,4,4v48a4,4,0,0,1-4,4H160a4,4,0,0,1-4-4Zm0-112a4,4,0,0,1,4-4h48a4,4,0,0,1,4,4V96a4,4,0,0,1-4,4H160a4,4,0,0,1-4-4Z"}))]]),W2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M125.18,156.94a64,64,0,1,0-82.36,0,100.23,100.23,0,0,0-39.49,32,12,12,0,0,0,19.35,14.2,76,76,0,0,1,122.64,0,12,12,0,0,0,19.36-14.2A100.33,100.33,0,0,0,125.18,156.94ZM44,108a40,40,0,1,1,40,40A40,40,0,0,1,44,108Zm206.1,97.67a12,12,0,0,1-16.78-2.57A76.31,76.31,0,0,0,172,172a12,12,0,0,1,0-24,40,40,0,1,0-10.3-78.67,12,12,0,1,1-6.16-23.19,64,64,0,0,1,57.64,110.8,100.23,100.23,0,0,1,39.49,32A12,12,0,0,1,250.1,205.67Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M136,108A52,52,0,1,1,84,56,52,52,0,0,1,136,108Z",opacity:"0.2"}),D.createElement("path",{d:"M117.25,157.92a60,60,0,1,0-66.5,0A95.83,95.83,0,0,0,3.53,195.63a8,8,0,1,0,13.4,8.74,80,80,0,0,1,134.14,0,8,8,0,0,0,13.4-8.74A95.83,95.83,0,0,0,117.25,157.92ZM40,108a44,44,0,1,1,44,44A44.05,44.05,0,0,1,40,108Zm210.14,98.7a8,8,0,0,1-11.07-2.33A79.83,79.83,0,0,0,172,168a8,8,0,0,1,0-16,44,44,0,1,0-16.34-84.87,8,8,0,1,1-5.94-14.85,60,60,0,0,1,55.53,105.64,95.83,95.83,0,0,1,47.22,37.71A8,8,0,0,1,250.14,206.7Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M164.47,195.63a8,8,0,0,1-6.7,12.37H10.23a8,8,0,0,1-6.7-12.37,95.83,95.83,0,0,1,47.22-37.71,60,60,0,1,1,66.5,0A95.83,95.83,0,0,1,164.47,195.63Zm87.91-.15a95.87,95.87,0,0,0-47.13-37.56A60,60,0,0,0,144.7,54.59a4,4,0,0,0-1.33,6A75.83,75.83,0,0,1,147,150.53a4,4,0,0,0,1.07,5.53,112.32,112.32,0,0,1,29.85,30.83,23.92,23.92,0,0,1,3.65,16.47,4,4,0,0,0,3.95,4.64h60.3a8,8,0,0,0,7.73-5.93A8.22,8.22,0,0,0,252.38,195.48Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M112.6,158.43a58,58,0,1,0-57.2,0A93.83,93.83,0,0,0,5.21,196.72a6,6,0,0,0,10.05,6.56,82,82,0,0,1,137.48,0,6,6,0,0,0,10-6.56A93.83,93.83,0,0,0,112.6,158.43ZM38,108a46,46,0,1,1,46,46A46.06,46.06,0,0,1,38,108Zm211,97a6,6,0,0,1-8.3-1.74A81.8,81.8,0,0,0,172,166a6,6,0,0,1,0-12,46,46,0,1,0-17.08-88.73,6,6,0,1,1-4.46-11.14,58,58,0,0,1,50.14,104.3,93.83,93.83,0,0,1,50.19,38.29A6,6,0,0,1,249,205Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M117.25,157.92a60,60,0,1,0-66.5,0A95.83,95.83,0,0,0,3.53,195.63a8,8,0,1,0,13.4,8.74,80,80,0,0,1,134.14,0,8,8,0,0,0,13.4-8.74A95.83,95.83,0,0,0,117.25,157.92ZM40,108a44,44,0,1,1,44,44A44.05,44.05,0,0,1,40,108Zm210.14,98.7a8,8,0,0,1-11.07-2.33A79.83,79.83,0,0,0,172,168a8,8,0,0,1,0-16,44,44,0,1,0-16.34-84.87,8,8,0,1,1-5.94-14.85,60,60,0,0,1,55.53,105.64,95.83,95.83,0,0,1,47.22,37.71A8,8,0,0,1,250.14,206.7Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M107.19,159a56,56,0,1,0-46.38,0A91.83,91.83,0,0,0,6.88,197.81a4,4,0,1,0,6.7,4.37,84,84,0,0,1,140.84,0,4,4,0,1,0,6.7-4.37A91.83,91.83,0,0,0,107.19,159ZM36,108a48,48,0,1,1,48,48A48.05,48.05,0,0,1,36,108Zm212,95.35a4,4,0,0,1-5.53-1.17A83.81,83.81,0,0,0,172,164a4,4,0,0,1,0-8,48,48,0,1,0-17.82-92.58,4,4,0,1,1-3-7.43,56,56,0,0,1,44,103,91.83,91.83,0,0,1,53.93,38.86A4,4,0,0,1,248,203.35Z"}))]]),V2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M240.26,186.1,152.81,34.23h0a28.74,28.74,0,0,0-49.62,0L15.74,186.1a27.45,27.45,0,0,0,0,27.71A28.31,28.31,0,0,0,40.55,228h174.9a28.31,28.31,0,0,0,24.79-14.19A27.45,27.45,0,0,0,240.26,186.1Zm-20.8,15.7a4.46,4.46,0,0,1-4,2.2H40.55a4.46,4.46,0,0,1-4-2.2,3.56,3.56,0,0,1,0-3.73L124,46.2a4.77,4.77,0,0,1,8,0l87.44,151.87A3.56,3.56,0,0,1,219.46,201.8ZM116,136V104a12,12,0,0,1,24,0v32a12,12,0,0,1-24,0Zm28,40a16,16,0,1,1-16-16A16,16,0,0,1,144,176Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M215.46,216H40.54C27.92,216,20,202.79,26.13,192.09L113.59,40.22c6.3-11,22.52-11,28.82,0l87.46,151.87C236,202.79,228.08,216,215.46,216Z",opacity:"0.2"}),D.createElement("path",{d:"M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM120,104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm8,88a12,12,0,1,1,12-12A12,12,0,0,1,128,192Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M235.07,189.09,147.61,37.22h0a22.75,22.75,0,0,0-39.22,0L20.93,189.09a21.53,21.53,0,0,0,0,21.72A22.35,22.35,0,0,0,40.55,222h174.9a22.35,22.35,0,0,0,19.6-11.19A21.53,21.53,0,0,0,235.07,189.09ZM224.66,204.8a10.46,10.46,0,0,1-9.21,5.2H40.55a10.46,10.46,0,0,1-9.21-5.2,9.51,9.51,0,0,1,0-9.72L118.79,43.21a10.75,10.75,0,0,1,18.42,0l87.46,151.87A9.51,9.51,0,0,1,224.66,204.8ZM122,144V104a6,6,0,0,1,12,0v40a6,6,0,0,1-12,0Zm16,36a10,10,0,1,1-10-10A10,10,0,0,1,138,180Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M233.34,190.09,145.88,38.22h0a20.75,20.75,0,0,0-35.76,0L22.66,190.09a19.52,19.52,0,0,0,0,19.71A20.36,20.36,0,0,0,40.54,220H215.46a20.36,20.36,0,0,0,17.86-10.2A19.52,19.52,0,0,0,233.34,190.09ZM226.4,205.8a12.47,12.47,0,0,1-10.94,6.2H40.54a12.47,12.47,0,0,1-10.94-6.2,11.45,11.45,0,0,1,0-11.72L117.05,42.21a12.76,12.76,0,0,1,21.9,0L226.4,194.08A11.45,11.45,0,0,1,226.4,205.8ZM124,144V104a4,4,0,0,1,8,0v40a4,4,0,0,1-8,0Zm12,36a8,8,0,1,1-8-8A8,8,0,0,1,136,180Z"}))]]),$2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm-12-80V80a12,12,0,0,1,24,0v52a12,12,0,0,1-24,0Zm28,40a16,16,0,1,1-16-16A16,16,0,0,1,144,172Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z",opacity:"0.2"}),D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm-8-80V80a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,172Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm-8,56a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0Zm8,104a12,12,0,1,1,12-12A12,12,0,0,1,128,184Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,26A102,102,0,1,0,230,128,102.12,102.12,0,0,0,128,26Zm0,192a90,90,0,1,1,90-90A90.1,90.1,0,0,1,128,218Zm-6-82V80a6,6,0,0,1,12,0v56a6,6,0,0,1-12,0Zm16,36a10,10,0,1,1-10-10A10,10,0,0,1,138,172Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm-8-80V80a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,172Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,28A100,100,0,1,0,228,128,100.11,100.11,0,0,0,128,28Zm0,192a92,92,0,1,1,92-92A92.1,92.1,0,0,1,128,220Zm-4-84V80a4,4,0,0,1,8,0v56a4,4,0,0,1-8,0Zm12,36a8,8,0,1,1-8-8A8,8,0,0,1,136,172Z"}))]]),z2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M230.47,67.5a12,12,0,0,0-19.26-4.32L172.43,99l-12.68-2.72L157,83.57l35.79-38.78a12,12,0,0,0-4.32-19.26A76.07,76.07,0,0,0,88.41,121.64L30.92,174.18a4.68,4.68,0,0,0-.39.38,36,36,0,0,0,50.91,50.91l.38-.39,52.54-57.49A76.05,76.05,0,0,0,230.47,67.5ZM160,148a51.5,51.5,0,0,1-23.35-5.52,12,12,0,0,0-14.26,2.62L64.31,208.66a12,12,0,0,1-17-17l63.55-58.07a12,12,0,0,0,2.62-14.26A51.5,51.5,0,0,1,108,96a52.06,52.06,0,0,1,52-52h.89L135.17,71.87a12,12,0,0,0-2.91,10.65l5.66,26.35a12,12,0,0,0,9.21,9.21l26.35,5.66a12,12,0,0,0,10.65-2.91L212,95.12c0,.3,0,.59,0,.89A52.06,52.06,0,0,1,160,148Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,96a64,64,0,0,1-94.94,56L73,217A24,24,0,0,1,39,183L104,126.94a64,64,0,0,1,80-90.29L144,80l5.66,26.34L176,112l43.35-40A63.8,63.8,0,0,1,224,96Z",opacity:"0.2"}),D.createElement("path",{d:"M226.76,69a8,8,0,0,0-12.84-2.88l-40.3,37.19-17.23-3.7-3.7-17.23,37.19-40.3A8,8,0,0,0,187,29.24,72,72,0,0,0,88,96,72.34,72.34,0,0,0,94,124.94L33.79,177c-.15.12-.29.26-.43.39a32,32,0,0,0,45.26,45.26c.13-.13.27-.28.39-.42L131.06,162A72,72,0,0,0,232,96,71.56,71.56,0,0,0,226.76,69ZM160,152a56.14,56.14,0,0,1-27.07-7,8,8,0,0,0-9.92,1.77L67.11,211.51a16,16,0,0,1-22.62-22.62L109.18,133a8,8,0,0,0,1.77-9.93,56,56,0,0,1,58.36-82.31l-31.2,33.81a8,8,0,0,0-1.94,7.1L141.83,108a8,8,0,0,0,6.14,6.14l26.35,5.66a8,8,0,0,0,7.1-1.94l33.81-31.2A56.06,56.06,0,0,1,160,152Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M232,96a72,72,0,0,1-100.94,66L79,222.22c-.12.14-.26.29-.39.42a32,32,0,0,1-45.26-45.26c.14-.13.28-.27.43-.39L94,124.94a72.07,72.07,0,0,1,83.54-98.78,8,8,0,0,1,3.93,13.19L144,80l5.66,26.35L176,112l40.65-37.52a8,8,0,0,1,13.19,3.93A72.6,72.6,0,0,1,232,96Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224.91,69.75a6,6,0,0,0-9.63-2.16l-41.07,37.9L154.7,101.3l-4.19-19.51,37.9-41.07a6,6,0,0,0-2.16-9.63,70,70,0,0,0-89.77,94.39l-61.39,53c-.11.09-.21.19-.32.3A30,30,0,0,0,77.2,221.23c.11-.11.21-.21.3-.32l53-61.39a70,70,0,0,0,94.39-89.77ZM160,154a58,58,0,0,1-28-7.22,6,6,0,0,0-7.45,1.33L68.57,212.88a18,18,0,0,1-25.45-25.45l64.76-55.94A6,6,0,0,0,109.2,124a58,58,0,0,1,64-84.53L139.58,75.93a6,6,0,0,0-1.45,5.33l5.65,26.35a6,6,0,0,0,4.61,4.61l26.35,5.65a6,6,0,0,0,5.33-1.45L216.49,82.8A58.06,58.06,0,0,1,160,154Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M226.76,69a8,8,0,0,0-12.84-2.88l-40.3,37.19-17.23-3.7-3.7-17.23,37.19-40.3A8,8,0,0,0,187,29.24,72,72,0,0,0,88,96,72.34,72.34,0,0,0,94,124.94L33.79,177c-.15.12-.29.26-.43.39a32,32,0,0,0,45.26,45.26c.13-.13.27-.28.39-.42L131.06,162A72,72,0,0,0,232,96,71.56,71.56,0,0,0,226.76,69ZM160,152a56.14,56.14,0,0,1-27.07-7,8,8,0,0,0-9.92,1.77L67.11,211.51a16,16,0,0,1-22.62-22.62L109.18,133a8,8,0,0,0,1.77-9.93,56,56,0,0,1,58.36-82.31l-31.2,33.81a8,8,0,0,0-1.94,7.1L141.83,108a8,8,0,0,0,6.14,6.14l26.35,5.66a8,8,0,0,0,7.1-1.94l33.81-31.2A56.06,56.06,0,0,1,160,152Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M223.05,70.5a4,4,0,0,0-6.42-1.44l-41.82,38.6L153,103l-4.68-21.79,38.6-41.82a4,4,0,0,0-1.44-6.43A68,68,0,0,0,98.94,126L36.4,180l-.21.2a28,28,0,0,0,39.6,39.6l.2-.21,54-62.54A68,68,0,0,0,228,96,67.51,67.51,0,0,0,223.05,70.5ZM160,156a60,60,0,0,1-29-7.47,4,4,0,0,0-5,.89L70,214.25A20,20,0,0,1,41.75,186l64.82-56a4,4,0,0,0,.89-5,60,60,0,0,1,69.46-86.59L141.05,77.29a4,4,0,0,0-1,3.55l5.66,26.35a4,4,0,0,0,3.07,3.07l26.35,5.66a4,4,0,0,0,3.55-1l38.87-35.87A60.05,60.05,0,0,1,160,156Z"}))]]),U2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208.49,191.51a12,12,0,0,1-17,17L128,145,64.49,208.49a12,12,0,0,1-17-17L111,128,47.51,64.49a12,12,0,0,1,17-17L128,111l63.51-63.52a12,12,0,0,1,17,17L145,128Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M216,56V200a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V56A16,16,0,0,1,56,40H200A16,16,0,0,1,216,56Z",opacity:"0.2"}),D.createElement("path",{d:"M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM181.66,170.34a8,8,0,0,1-11.32,11.32L128,139.31,85.66,181.66a8,8,0,0,1-11.32-11.32L116.69,128,74.34,85.66A8,8,0,0,1,85.66,74.34L128,116.69l42.34-42.35a8,8,0,0,1,11.32,11.32L139.31,128Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M204.24,195.76a6,6,0,1,1-8.48,8.48L128,136.49,60.24,204.24a6,6,0,0,1-8.48-8.48L119.51,128,51.76,60.24a6,6,0,0,1,8.48-8.48L128,119.51l67.76-67.75a6,6,0,0,1,8.48,8.48L136.49,128Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M202.83,197.17a4,4,0,0,1-5.66,5.66L128,133.66,58.83,202.83a4,4,0,0,1-5.66-5.66L122.34,128,53.17,58.83a4,4,0,0,1,5.66-5.66L128,122.34l69.17-69.17a4,4,0,1,1,5.66,5.66L133.66,128Z"}))]]),q2r=new Map([["bold",D.createElement(D.Fragment,null,D.createElement("path",{d:"M168.49,104.49,145,128l23.52,23.51a12,12,0,0,1-17,17L128,145l-23.51,23.52a12,12,0,0,1-17-17L111,128,87.51,104.49a12,12,0,0,1,17-17L128,111l23.51-23.52a12,12,0,0,1,17,17ZM236,128A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-24,0a84,84,0,1,0-84,84A84.09,84.09,0,0,0,212,128Z"}))],["duotone",D.createElement(D.Fragment,null,D.createElement("path",{d:"M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z",opacity:"0.2"}),D.createElement("path",{d:"M165.66,101.66,139.31,128l26.35,26.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"}))],["fill",D.createElement(D.Fragment,null,D.createElement("path",{d:"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm37.66,130.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32L139.31,128Z"}))],["light",D.createElement(D.Fragment,null,D.createElement("path",{d:"M164.24,100.24,136.48,128l27.76,27.76a6,6,0,1,1-8.48,8.48L128,136.48l-27.76,27.76a6,6,0,0,1-8.48-8.48L119.52,128,91.76,100.24a6,6,0,0,1,8.48-8.48L128,119.52l27.76-27.76a6,6,0,0,1,8.48,8.48ZM230,128A102,102,0,1,1,128,26,102.12,102.12,0,0,1,230,128Zm-12,0a90,90,0,1,0-90,90A90.1,90.1,0,0,0,218,128Z"}))],["regular",D.createElement(D.Fragment,null,D.createElement("path",{d:"M165.66,101.66,139.31,128l26.35,26.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"}))],["thin",D.createElement(D.Fragment,null,D.createElement("path",{d:"M162.83,98.83,133.66,128l29.17,29.17a4,4,0,0,1-5.66,5.66L128,133.66,98.83,162.83a4,4,0,0,1-5.66-5.66L122.34,128,93.17,98.83a4,4,0,0,1,5.66-5.66L128,122.34l29.17-29.17a4,4,0,1,1,5.66,5.66ZM228,128A100,100,0,1,1,128,28,100.11,100.11,0,0,1,228,128Zm-8,0a92,92,0,1,0-92,92A92.1,92.1,0,0,0,220,128Z"}))]]),G2r=D.createContext({color:"currentColor",size:"1em",weight:"regular",mirrored:!1}),mo=D.forwardRef((n,e)=>{const{alt:t,color:i,size:r,weight:o,mirrored:l,children:c,weights:d,...h}=n,{color:p="currentColor",size:m,weight:b="regular",mirrored:w=!1,..._}=D.useContext(G2r);return D.createElement("svg",{ref:e,xmlns:"http://www.w3.org/2000/svg",width:r??m,height:r??m,fill:i??p,viewBox:"0 0 256 256",transform:l||w?"scale(-1, 1)":void 0,..._,...h},!!t&&D.createElement("title",null,t),c,d.get(o??b))});mo.displayName="IconBase";const Oui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:__r}));Oui.displayName="ArrowArcRightIcon";const K2r=Oui,Nui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:C_r}));Nui.displayName="ArrowClockwiseIcon";const _5e=Nui,Pui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:S_r}));Pui.displayName="ArrowCounterClockwiseIcon";const cGe=Pui,Fui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:x_r}));Fui.displayName="ArrowDownIcon";const Y2r=Fui,jui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:E_r}));jui.displayName="ArrowLeftIcon";const Hui=jui,Bui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:k_r}));Bui.displayName="ArrowRightIcon";const k9=Bui,Wui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:T_r}));Wui.displayName="ArrowSquareOutIcon";const sNt=Wui,Vui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:L_r}));Vui.displayName="ArrowUUpLeftIcon";const g9n=Vui,$ui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:D_r}));$ui.displayName="ArrowsInSimpleIcon";const Z2r=$ui,zui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:I_r}));zui.displayName="AsteriskIcon";const m9n=zui,Uui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:A_r}));Uui.displayName="BrainIcon";const X2r=Uui,yUt=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:R_r}));yUt.displayName="BroadcastIcon";const qui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:M_r}));qui.displayName="CardsIcon";const Gui=qui,Kui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:O_r}));Kui.displayName="CaretDoubleRightIcon";const Q2r=Kui,Yui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:N_r}));Yui.displayName="CaretDownIcon";const X1e=Yui,Zui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:P_r}));Zui.displayName="CaretRightIcon";const Xui=Zui,Qui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:F_r}));Qui.displayName="CaretUpIcon";const J2r=Qui,Jui=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:j_r}));Jui.displayName="ChatCircleIcon";const b9n=Jui,edi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:H_r}));edi.displayName="CheckIcon";const _Ut=edi,tdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:B_r}));tdi.displayName="CheckCircleIcon";const CUt=tdi,ndi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:W_r}));ndi.displayName="ClockIcon";const SUt=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:V_r}));SUt.displayName="ClockClockwiseIcon";const idi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:$_r}));idi.displayName="CloudArrowDownIcon";const rdi=idi,sdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:z_r}));sdi.displayName="CodeIcon";const eCr=sdi,odi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:U_r}));odi.displayName="ColumnsIcon";const tCr=odi,adi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:q_r}));adi.displayName="CopyIcon";const nCr=adi,ldi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:G_r}));ldi.displayName="CopySimpleIcon";const cdi=ldi,udi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:K_r}));udi.displayName="CornersOutIcon";const iCr=udi,ddi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:Y_r}));ddi.displayName="CpuIcon";const rCr=ddi,hdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:Z_r}));hdi.displayName="DiamondIcon";const xUt=hdi,fdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:X_r}));fdi.displayName="DotsThreeIcon";const sCr=fdi,pdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:Q_r}));pdi.displayName="DownloadIcon";const oCr=pdi,gdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:J_r}));gdi.displayName="FileIcon";const aCr=gdi,EUt=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:e2r}));EUt.displayName="FileJsIcon";const mdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:t2r}));mdi.displayName="FileMagnifyingGlassIcon";const bdi=mdi,vdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:n2r}));vdi.displayName="FileTextIcon";const lCr=vdi,kUt=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:i2r}));kUt.displayName="FilesIcon";const wdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:r2r}));wdi.displayName="FlagIcon";const cCr=wdi,ydi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:s2r}));ydi.displayName="FolderSimpleIcon";const uCr=ydi,_di=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:o2r}));_di.displayName="FunctionIcon";const Cdi=_di,TUt=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:a2r}));TUt.displayName="GearIcon";const dCr=TUt,Sdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:l2r}));Sdi.displayName="GitForkIcon";const LUt=Sdi,xdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:c2r}));xdi.displayName="GlobeIcon";const uGe=xdi,Edi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:u2r}));Edi.displayName="GraphIcon";const hCr=Edi,kdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:d2r}));kdi.displayName="GridFourIcon";const fCr=kdi,DUt=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:h2r}));DUt.displayName="HandshakeIcon";const Tdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:f2r}));Tdi.displayName="HourglassIcon";const Ldi=Tdi,Ddi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:p2r}));Ddi.displayName="KeyIcon";const pCr=Ddi,Idi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:g2r}));Idi.displayName="LinkIcon";const Xet=Idi,Adi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:m2r}));Adi.displayName="ListIcon";const Rdi=Adi,Mdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:b2r}));Mdi.displayName="ListBulletsIcon";const gCr=Mdi,Odi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:v2r}));Odi.displayName="ListPlusIcon";const mCr=Odi,Ndi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:w2r}));Ndi.displayName="MagnifyingGlassIcon";const ise=Ndi,Pdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:y2r}));Pdi.displayName="NotePencilIcon";const Fdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:_2r}));Fdi.displayName="PauseIcon";const Qet=Fdi,jdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:C2r}));jdi.displayName="PauseCircleIcon";const bCr=jdi,Hdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:S2r}));Hdi.displayName="PencilSimpleIcon";const Bdi=Hdi,Wdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:x2r}));Wdi.displayName="PersonIcon";const Vdi=Wdi,IUt=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:E2r}));IUt.displayName="PersonSimpleRunIcon";const $di=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:k2r}));$di.displayName="PlayIcon";const vCr=$di,zdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:T2r}));zdi.displayName="PlusIcon";const AUt=zdi,Udi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:L2r}));Udi.displayName="ProhibitIcon";const qdi=Udi,Gdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:D2r}));Gdi.displayName="RepeatIcon";const RUt=Gdi,Kdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:I2r}));Kdi.displayName="RobotIcon";const wCr=Kdi,Ydi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:A2r}));Ydi.displayName="RocketLaunchIcon";const Zdi=Ydi,MUt=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:R2r}));MUt.displayName="RowsIcon";const Xdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:M2r}));Xdi.displayName="ScissorsIcon";const OUt=Xdi,Qdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:O2r}));Qdi.displayName="ShieldCheckIcon";const NUt=Qdi,Jdi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:N2r}));Jdi.displayName="ShieldWarningIcon";const yCr=Jdi,ehi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:P2r}));ehi.displayName="SpinnerIcon";const _Cr=ehi,thi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:F2r}));thi.displayName="StopIcon";const VEt=thi,nhi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:j2r}));nhi.displayName="TagIcon";const ihi=nhi,rhi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:H2r}));rhi.displayName="TrashIcon";const PUt=rhi,shi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:B2r}));shi.displayName="TreeStructureIcon";const CCr=shi,ohi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:W2r}));ohi.displayName="UsersIcon";const SCr=ohi,ahi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:V2r}));ahi.displayName="WarningIcon";const xCr=ahi,lhi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:$2r}));lhi.displayName="WarningCircleIcon";const ECr=lhi,chi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:z2r}));chi.displayName="WrenchIcon";const kCr=chi,uhi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:U2r}));uhi.displayName="XIcon";const X6=uhi,dhi=D.forwardRef((n,e)=>D.createElement(mo,{ref:e,...n,weights:q2r}));dhi.displayName="XCircleIcon";const rse=dhi,v9n={customErrorColor:{background:"#fdeded",color:"#622524"},customWarningColor:{backgroundColor:"#FBA404"}},rC=({message:n,onDismiss:e,severity:t="info",sx:i={},anchorOrigin:r={vertical:"top",horizontal:"center"},autoHideDuration:o=3e3,id:l,action:c})=>{const d=!!n;return k.jsx(Rui,{anchorOrigin:r,onClose:()=>e&&e(),open:d,autoHideDuration:o,sx:i,children:k.jsx(_W,{icon:t==="error"?k.jsx(ECr,{color:"red"}):"",variant:"filled",elevation:6,onClose:()=>e&&e(),severity:t,sx:t==="error"?v9n.customErrorColor:void 0,id:l,style:t==="warning"?v9n.customWarningColor:void 0,action:c,children:n})})},w9n=null,TCr=({children:n})=>{const[e,t]=D.useState(w9n);return k.jsxs(k.Fragment,{children:[e?k.jsx(rC,{id:"global-snackbar-message",message:e.text,severity:e.severity,onDismiss:()=>t(w9n)}):null,k.jsx(c7.Provider,{value:{setMessage:t},children:n})]})};var qBe={},y9n;function LCr(){if(y9n)return qBe;y9n=1;var n=qzt();return qBe.createRoot=n.createRoot,qBe.hydrateRoot=n.hydrateRoot,qBe}var DCr=LCr();function oNt(){return oNt=Object.assign?Object.assign.bind():function(n){for(var e=1;e'),!0):e?n.some(function(t){return e.includes(t)})||n.includes("*"):!0}var jCr=function(e,t,i){i===void 0&&(i=!1);var r=t.alt,o=t.meta,l=t.mod,c=t.shift,d=t.ctrl,h=t.keys,p=e.key,m=e.code,b=e.ctrlKey,w=e.metaKey,_=e.shiftKey,x=e.altKey,T=PG(m),I=p.toLowerCase();if(!(h!=null&&h.includes(T))&&!(h!=null&&h.includes(I))&&!["ctrl","control","unknown","meta","alt","shift","os"].includes(T))return!1;if(!i){if(r===!x&&I!=="alt"||c===!_&&I!=="shift")return!1;if(l){if(!w&&!b)return!1}else if(o===!w&&I!=="meta"&&I!=="os"||d===!b&&I!=="ctrl"&&I!=="control")return!1}return h&&h.length===1&&(h.includes(I)||h.includes(T))?!0:h?RCr(h):!h},mhi=D.createContext(void 0),HCr=function(){return D.useContext(mhi)};function BCr(n){var e=n.addHotkey,t=n.removeHotkey,i=n.children;return k.jsx(mhi.Provider,{value:{addHotkey:e,removeHotkey:t},children:i})}function jUt(n,e){return n&&e&&typeof n=="object"&&typeof e=="object"?Object.keys(n).length===Object.keys(e).length&&Object.keys(n).reduce(function(t,i){return t&&jUt(n[i],e[i])},!0):n===e}var bhi=D.createContext({hotkeys:[],enabledScopes:[],toggleScope:function(){},enableScope:function(){},disableScope:function(){}}),WCr=function(){return D.useContext(bhi)},VCr=function(e){var t=e.initiallyActiveScopes,i=t===void 0?["*"]:t,r=e.children,o=D.useState(i?.length>0?i:["*"]),l=o[0],c=o[1],d=D.useState([]),h=d[0],p=d[1],m=D.useCallback(function(T){c(function(I){return I.includes("*")?[T]:Array.from(new Set([].concat(I,[T])))})},[]),b=D.useCallback(function(T){c(function(I){return I.filter(function(L){return L!==T}).length===0?["*"]:I.filter(function(L){return L!==T})})},[]),w=D.useCallback(function(T){c(function(I){return I.includes(T)?I.filter(function(L){return L!==T}).length===0?["*"]:I.filter(function(L){return L!==T}):I.includes("*")?[T]:Array.from(new Set([].concat(I,[T])))})},[]),_=D.useCallback(function(T){p(function(I){return[].concat(I,[T])})},[]),x=D.useCallback(function(T){p(function(I){return I.filter(function(L){return!jUt(L,T)})})},[]);return k.jsx(bhi.Provider,{value:{enabledScopes:l,hotkeys:h,enableScope:m,disableScope:b,toggleScope:w},children:k.jsx(BCr,{addHotkey:_,removeHotkey:x,children:r})})};function $Cr(n){var e=D.useRef(void 0);return jUt(e.current,n)||(e.current=n),e.current}var _9n=function(e){e.stopPropagation(),e.preventDefault(),e.stopImmediatePropagation()},zCr=typeof window<"u"?D.useLayoutEffect:D.useEffect;function nY(n,e,t,i){var r=D.useState(null),o=r[0],l=r[1],c=D.useRef(!1),d=t instanceof Array?i instanceof Array?void 0:i:t,h=FUt(n)?n.join(d?.splitKey):n,p=t instanceof Array?t:i instanceof Array?i:void 0,m=D.useCallback(e,p??[]),b=D.useRef(m);p?b.current=m:b.current=e;var w=$Cr(d),_=WCr(),x=_.enabledScopes,T=HCr();return zCr(function(){if(!(w?.enabled===!1||!FCr(x,w?.scopes))){var I=function(F,j){var W;if(j===void 0&&(j=!1),!(NCr(F)&&!ghi(F,w?.enableOnFormTags))){if(o!==null){var q=o.getRootNode();if((q instanceof Document||q instanceof ShadowRoot)&&q.activeElement!==o&&!o.contains(q.activeElement)){_9n(F);return}}(W=F.target)!=null&&W.isContentEditable&&!(w!=null&&w.enableOnContentEditable)||$Et(h,w?.splitKey).forEach(function(Z){var ee,G=zEt(Z,w?.combinationKey);if(jCr(F,G,w?.ignoreModifiers)||(ee=G.keys)!=null&&ee.includes("*")){if(w!=null&&w.ignoreEventWhen!=null&&w.ignoreEventWhen(F)||j&&c.current)return;if(MCr(F,G,w?.preventDefault),!OCr(F,G,w?.enabled)){_9n(F);return}b.current(F,G),j||(c.current=!0)}})}},L=function(F){F.key!==void 0&&(fhi(PG(F.code)),(w?.keydown===void 0&&w?.keyup!==!0||w!=null&&w.keydown)&&I(F))},A=function(F){F.key!==void 0&&(phi(PG(F.code)),c.current=!1,w!=null&&w.keyup&&I(F,!0))},M=o||d?.document||document;return M.addEventListener("keyup",A,d?.eventListenerOptions),M.addEventListener("keydown",L,d?.eventListenerOptions),T&&$Et(h,w?.splitKey).forEach(function(O){return T.addHotkey(zEt(O,w?.combinationKey,w?.description))}),function(){M.removeEventListener("keyup",A,d?.eventListenerOptions),M.removeEventListener("keydown",L,d?.eventListenerOptions),T&&$Et(h,w?.splitKey).forEach(function(O){return T.removeHotkey(zEt(O,w?.combinationKey,w?.description))})}}},[o,h,w,x]),l}var Q1e=(function(){function n(){this.listeners=[]}var e=n.prototype;return e.subscribe=function(i){var r=this,o=i||function(){};return this.listeners.push(o),this.onSubscribe(),function(){r.listeners=r.listeners.filter(function(l){return l!==o}),r.onUnsubscribe()}},e.hasListeners=function(){return this.listeners.length>0},e.onSubscribe=function(){},e.onUnsubscribe=function(){},n})(),dGe=typeof window>"u";function V2(){}function UCr(n,e){return typeof n=="function"?n(e):n}function aNt(n){return typeof n=="number"&&n>=0&&n!==1/0}function hGe(n){return Array.isArray(n)?n:[n]}function vhi(n,e){return Math.max(n+(e||0)-Date.now(),0)}function _Ue(n,e,t){return C5e(n)?typeof e=="function"?Zt({},t,{queryKey:n,queryFn:e}):Zt({},e,{queryKey:n}):n}function qCr(n,e,t){return C5e(n)?typeof e=="function"?Zt({},t,{mutationKey:n,mutationFn:e}):Zt({},e,{mutationKey:n}):typeof n=="function"?Zt({},e,{mutationFn:n}):Zt({},n)}function nG(n,e,t){return C5e(n)?[Zt({},e,{queryKey:n}),t]:[n||{},e]}function GCr(n,e){if(n===!0&&e===!0||n==null&&e==null)return"all";if(n===!1&&e===!1)return"none";var t=n??!e;return t?"active":"inactive"}function C9n(n,e){var t=n.active,i=n.exact,r=n.fetching,o=n.inactive,l=n.predicate,c=n.queryKey,d=n.stale;if(C5e(c)){if(i){if(e.queryHash!==HUt(c,e.options))return!1}else if(!fGe(e.queryKey,c))return!1}var h=GCr(t,o);if(h==="none")return!1;if(h!=="all"){var p=e.isActive();if(h==="active"&&!p||h==="inactive"&&p)return!1}return!(typeof d=="boolean"&&e.isStale()!==d||typeof r=="boolean"&&e.isFetching()!==r||l&&!l(e))}function S9n(n,e){var t=n.exact,i=n.fetching,r=n.predicate,o=n.mutationKey;if(C5e(o)){if(!e.options.mutationKey)return!1;if(t){if(Une(e.options.mutationKey)!==Une(o))return!1}else if(!fGe(e.options.mutationKey,o))return!1}return!(typeof i=="boolean"&&e.state.status==="loading"!==i||r&&!r(e))}function HUt(n,e){var t=e?.queryKeyHashFn||Une;return t(n)}function Une(n){var e=hGe(n);return KCr(e)}function KCr(n){return JSON.stringify(n,function(e,t){return lNt(t)?Object.keys(t).sort().reduce(function(i,r){return i[r]=t[r],i},{}):t})}function fGe(n,e){return whi(hGe(n),hGe(e))}function whi(n,e){return n===e?!0:typeof n!=typeof e?!1:n&&e&&typeof n=="object"&&typeof e=="object"?!Object.keys(e).some(function(t){return!whi(n[t],e[t])}):!1}function pGe(n,e){if(n===e)return n;var t=Array.isArray(n)&&Array.isArray(e);if(t||lNt(n)&&lNt(e)){for(var i=t?n.length:Object.keys(n).length,r=t?e:Object.keys(e),o=r.length,l=t?[]:{},c=0,d=0;d"u")return!0;var t=e.prototype;return!(!x9n(t)||!t.hasOwnProperty("isPrototypeOf"))}function x9n(n){return Object.prototype.toString.call(n)==="[object Object]"}function C5e(n){return typeof n=="string"||Array.isArray(n)}function ZCr(n){return new Promise(function(e){setTimeout(e,n)})}function E9n(n){Promise.resolve().then(n).catch(function(e){return setTimeout(function(){throw e})})}function yhi(){if(typeof AbortController=="function")return new AbortController}var XCr=(function(n){ZW(e,n);function e(){var i;return i=n.call(this)||this,i.setup=function(r){var o;if(!dGe&&((o=window)!=null&&o.addEventListener)){var l=function(){return r()};return window.addEventListener("visibilitychange",l,!1),window.addEventListener("focus",l,!1),function(){window.removeEventListener("visibilitychange",l),window.removeEventListener("focus",l)}}},i}var t=e.prototype;return t.onSubscribe=function(){this.cleanup||this.setEventListener(this.setup)},t.onUnsubscribe=function(){if(!this.hasListeners()){var r;(r=this.cleanup)==null||r.call(this),this.cleanup=void 0}},t.setEventListener=function(r){var o,l=this;this.setup=r,(o=this.cleanup)==null||o.call(this),this.cleanup=r(function(c){typeof c=="boolean"?l.setFocused(c):l.onFocus()})},t.setFocused=function(r){this.focused=r,r&&this.onFocus()},t.onFocus=function(){this.listeners.forEach(function(r){r()})},t.isFocused=function(){return typeof this.focused=="boolean"?this.focused:typeof document>"u"?!0:[void 0,"visible","prerender"].includes(document.visibilityState)},e})(Q1e),d4e=new XCr,QCr=(function(n){ZW(e,n);function e(){var i;return i=n.call(this)||this,i.setup=function(r){var o;if(!dGe&&((o=window)!=null&&o.addEventListener)){var l=function(){return r()};return window.addEventListener("online",l,!1),window.addEventListener("offline",l,!1),function(){window.removeEventListener("online",l),window.removeEventListener("offline",l)}}},i}var t=e.prototype;return t.onSubscribe=function(){this.cleanup||this.setEventListener(this.setup)},t.onUnsubscribe=function(){if(!this.hasListeners()){var r;(r=this.cleanup)==null||r.call(this),this.cleanup=void 0}},t.setEventListener=function(r){var o,l=this;this.setup=r,(o=this.cleanup)==null||o.call(this),this.cleanup=r(function(c){typeof c=="boolean"?l.setOnline(c):l.onOnline()})},t.setOnline=function(r){this.online=r,r&&this.onOnline()},t.onOnline=function(){this.listeners.forEach(function(r){r()})},t.isOnline=function(){return typeof this.online=="boolean"?this.online:typeof navigator>"u"||typeof navigator.onLine>"u"?!0:navigator.onLine},e})(Q1e),CUe=new QCr;function JCr(n){return Math.min(1e3*Math.pow(2,n),3e4)}function gGe(n){return typeof n?.cancel=="function"}var _hi=function(e){this.revert=e?.revert,this.silent=e?.silent};function SUe(n){return n instanceof _hi}var Chi=function(e){var t=this,i=!1,r,o,l,c;this.abort=e.abort,this.cancel=function(b){return r?.(b)},this.cancelRetry=function(){i=!0},this.continueRetry=function(){i=!1},this.continue=function(){return o?.()},this.failureCount=0,this.isPaused=!1,this.isResolved=!1,this.isTransportCancelable=!1,this.promise=new Promise(function(b,w){l=b,c=w});var d=function(w){t.isResolved||(t.isResolved=!0,e.onSuccess==null||e.onSuccess(w),o?.(),l(w))},h=function(w){t.isResolved||(t.isResolved=!0,e.onError==null||e.onError(w),o?.(),c(w))},p=function(){return new Promise(function(w){o=w,t.isPaused=!0,e.onPause==null||e.onPause()}).then(function(){o=void 0,t.isPaused=!1,e.onContinue==null||e.onContinue()})},m=function b(){if(!t.isResolved){var w;try{w=e.fn()}catch(_){w=Promise.reject(_)}r=function(x){if(!t.isResolved&&(h(new _hi(x)),t.abort==null||t.abort(),gGe(w)))try{w.cancel()}catch{}},t.isTransportCancelable=gGe(w),Promise.resolve(w).then(d).catch(function(_){var x,T;if(!t.isResolved){var I=(x=e.retry)!=null?x:3,L=(T=e.retryDelay)!=null?T:JCr,A=typeof L=="function"?L(t.failureCount,_):L,M=I===!0||typeof I=="number"&&t.failureCount"u"&&(c.exact=!0),this.queries.find(function(d){return C9n(c,d)})},t.findAll=function(r,o){var l=nG(r,o),c=l[0];return Object.keys(c).length>0?this.queries.filter(function(d){return C9n(c,d)}):this.queries},t.notify=function(r){var o=this;D1.batch(function(){o.listeners.forEach(function(l){l(r)})})},t.onFocus=function(){var r=this;D1.batch(function(){r.queries.forEach(function(o){o.onFocus()})})},t.onOnline=function(){var r=this;D1.batch(function(){r.queries.forEach(function(o){o.onOnline()})})},e})(Q1e),rSr=(function(){function n(t){this.options=Zt({},t.defaultOptions,t.options),this.mutationId=t.mutationId,this.mutationCache=t.mutationCache,this.observers=[],this.state=t.state||xhi(),this.meta=t.meta}var e=n.prototype;return e.setState=function(i){this.dispatch({type:"setState",state:i})},e.addObserver=function(i){this.observers.indexOf(i)===-1&&this.observers.push(i)},e.removeObserver=function(i){this.observers=this.observers.filter(function(r){return r!==i})},e.cancel=function(){return this.retryer?(this.retryer.cancel(),this.retryer.promise.then(V2).catch(V2)):Promise.resolve()},e.continue=function(){return this.retryer?(this.retryer.continue(),this.retryer.promise):this.execute()},e.execute=function(){var i=this,r,o=this.state.status==="loading",l=Promise.resolve();return o||(this.dispatch({type:"loading",variables:this.options.variables}),l=l.then(function(){i.mutationCache.config.onMutate==null||i.mutationCache.config.onMutate(i.state.variables,i)}).then(function(){return i.options.onMutate==null?void 0:i.options.onMutate(i.state.variables)}).then(function(c){c!==i.state.context&&i.dispatch({type:"loading",context:c,variables:i.state.variables})})),l.then(function(){return i.executeMutation()}).then(function(c){r=c,i.mutationCache.config.onSuccess==null||i.mutationCache.config.onSuccess(r,i.state.variables,i.state.context,i)}).then(function(){return i.options.onSuccess==null?void 0:i.options.onSuccess(r,i.state.variables,i.state.context)}).then(function(){return i.options.onSettled==null?void 0:i.options.onSettled(r,null,i.state.variables,i.state.context)}).then(function(){return i.dispatch({type:"success",data:r}),r}).catch(function(c){return i.mutationCache.config.onError==null||i.mutationCache.config.onError(c,i.state.variables,i.state.context,i),mGe().error(c),Promise.resolve().then(function(){return i.options.onError==null?void 0:i.options.onError(c,i.state.variables,i.state.context)}).then(function(){return i.options.onSettled==null?void 0:i.options.onSettled(void 0,c,i.state.variables,i.state.context)}).then(function(){throw i.dispatch({type:"error",error:c}),c})})},e.executeMutation=function(){var i=this,r;return this.retryer=new Chi({fn:function(){return i.options.mutationFn?i.options.mutationFn(i.state.variables):Promise.reject("No mutationFn found")},onFail:function(){i.dispatch({type:"failed"})},onPause:function(){i.dispatch({type:"pause"})},onContinue:function(){i.dispatch({type:"continue"})},retry:(r=this.options.retry)!=null?r:0,retryDelay:this.options.retryDelay}),this.retryer.promise},e.dispatch=function(i){var r=this;this.state=sSr(this.state,i),D1.batch(function(){r.observers.forEach(function(o){o.onMutationUpdate(i)}),r.mutationCache.notify(r)})},n})();function xhi(){return{context:void 0,data:void 0,error:null,failureCount:0,isPaused:!1,status:"idle",variables:void 0}}function sSr(n,e){switch(e.type){case"failed":return Zt({},n,{failureCount:n.failureCount+1});case"pause":return Zt({},n,{isPaused:!0});case"continue":return Zt({},n,{isPaused:!1});case"loading":return Zt({},n,{context:e.context,data:void 0,error:null,isPaused:!1,status:"loading",variables:e.variables});case"success":return Zt({},n,{data:e.data,error:null,status:"success",isPaused:!1});case"error":return Zt({},n,{data:void 0,error:e.error,failureCount:n.failureCount+1,isPaused:!1,status:"error"});case"setState":return Zt({},n,e.state);default:return n}}var oSr=(function(n){ZW(e,n);function e(i){var r;return r=n.call(this)||this,r.config=i||{},r.mutations=[],r.mutationId=0,r}var t=e.prototype;return t.build=function(r,o,l){var c=new rSr({mutationCache:this,mutationId:++this.mutationId,options:r.defaultMutationOptions(o),state:l,defaultOptions:o.mutationKey?r.getMutationDefaults(o.mutationKey):void 0,meta:o.meta});return this.add(c),c},t.add=function(r){this.mutations.push(r),this.notify(r)},t.remove=function(r){this.mutations=this.mutations.filter(function(o){return o!==r}),r.cancel(),this.notify(r)},t.clear=function(){var r=this;D1.batch(function(){r.mutations.forEach(function(o){r.remove(o)})})},t.getAll=function(){return this.mutations},t.find=function(r){return typeof r.exact>"u"&&(r.exact=!0),this.mutations.find(function(o){return S9n(r,o)})},t.findAll=function(r){return this.mutations.filter(function(o){return S9n(r,o)})},t.notify=function(r){var o=this;D1.batch(function(){o.listeners.forEach(function(l){l(r)})})},t.onFocus=function(){this.resumePausedMutations()},t.onOnline=function(){this.resumePausedMutations()},t.resumePausedMutations=function(){var r=this.mutations.filter(function(o){return o.state.isPaused});return D1.batch(function(){return r.reduce(function(o,l){return o.then(function(){return l.continue().catch(V2)})},Promise.resolve())})},e})(Q1e);function aSr(){return{onFetch:function(e){e.fetchFn=function(){var t,i,r,o,l,c,d=(t=e.fetchOptions)==null||(i=t.meta)==null?void 0:i.refetchPage,h=(r=e.fetchOptions)==null||(o=r.meta)==null?void 0:o.fetchMore,p=h?.pageParam,m=h?.direction==="forward",b=h?.direction==="backward",w=((l=e.state.data)==null?void 0:l.pages)||[],_=((c=e.state.data)==null?void 0:c.pageParams)||[],x=yhi(),T=x?.signal,I=_,L=!1,A=e.options.queryFn||function(){return Promise.reject("Missing queryFn")},M=function(Q,ie,se,de){return I=de?[ie].concat(I):[].concat(I,[ie]),de?[se].concat(Q):[].concat(Q,[se])},O=function(Q,ie,se,de){if(L)return Promise.reject("Cancelled");if(typeof se>"u"&&!ie&&Q.length)return Promise.resolve(Q);var ne={queryKey:e.queryKey,signal:T,pageParam:se,meta:e.meta},we=A(ne),ue=Promise.resolve(we).then(function(ye){return M(Q,se,ye,de)});if(gGe(we)){var ce=ue;ce.cancel=we.cancel}return ue},F;if(!w.length)F=O([]);else if(m){var j=typeof p<"u",W=j?p:k9n(e.options,w);F=O(w,j,W)}else if(b){var q=typeof p<"u",Z=q?p:lSr(e.options,w);F=O(w,q,Z,!0)}else(function(){I=[];var te=typeof e.options.getNextPageParam>"u",Q=d&&w[0]?d(w[0],0,w):!0;F=Q?O([],te,_[0]):Promise.resolve(M([],_[0],w[0]));for(var ie=function(ne){F=F.then(function(we){var ue=d&&w[ne]?d(w[ne],ne,w):!0;if(ue){var ce=te?_[ne]:k9n(e.options,we);return O(we,te,ce)}return Promise.resolve(M(we,_[ne],w[ne]))})},se=1;se"u"&&(p.revert=!0);var m=D1.batch(function(){return l.queryCache.findAll(d).map(function(b){return b.cancel(p)})});return Promise.all(m).then(V2).catch(V2)},e.invalidateQueries=function(i,r,o){var l,c,d,h=this,p=nG(i,r,o),m=p[0],b=p[1],w=Zt({},m,{active:(l=(c=m.refetchActive)!=null?c:m.active)!=null?l:!0,inactive:(d=m.refetchInactive)!=null?d:!1});return D1.batch(function(){return h.queryCache.findAll(m).forEach(function(_){_.invalidate()}),h.refetchQueries(w,b)})},e.refetchQueries=function(i,r,o){var l=this,c=nG(i,r,o),d=c[0],h=c[1],p=D1.batch(function(){return l.queryCache.findAll(d).map(function(b){return b.fetch(void 0,Zt({},h,{meta:{refetchPage:d?.refetchPage}}))})}),m=Promise.all(p).then(V2);return h?.throwOnError||(m=m.catch(V2)),m},e.fetchQuery=function(i,r,o){var l=_Ue(i,r,o),c=this.defaultQueryOptions(l);typeof c.retry>"u"&&(c.retry=!1);var d=this.queryCache.build(this,c);return d.isStaleByTime(c.staleTime)?d.fetch(c):Promise.resolve(d.state.data)},e.prefetchQuery=function(i,r,o){return this.fetchQuery(i,r,o).then(V2).catch(V2)},e.fetchInfiniteQuery=function(i,r,o){var l=_Ue(i,r,o);return l.behavior=aSr(),this.fetchQuery(l)},e.prefetchInfiniteQuery=function(i,r,o){return this.fetchInfiniteQuery(i,r,o).then(V2).catch(V2)},e.cancelMutations=function(){var i=this,r=D1.batch(function(){return i.mutationCache.getAll().map(function(o){return o.cancel()})});return Promise.all(r).then(V2).catch(V2)},e.resumePausedMutations=function(){return this.getMutationCache().resumePausedMutations()},e.executeMutation=function(i){return this.mutationCache.build(this,i).execute()},e.getQueryCache=function(){return this.queryCache},e.getMutationCache=function(){return this.mutationCache},e.getDefaultOptions=function(){return this.defaultOptions},e.setDefaultOptions=function(i){this.defaultOptions=i},e.setQueryDefaults=function(i,r){var o=this.queryDefaults.find(function(l){return Une(i)===Une(l.queryKey)});o?o.defaultOptions=r:this.queryDefaults.push({queryKey:i,defaultOptions:r})},e.getQueryDefaults=function(i){var r;return i?(r=this.queryDefaults.find(function(o){return fGe(i,o.queryKey)}))==null?void 0:r.defaultOptions:void 0},e.setMutationDefaults=function(i,r){var o=this.mutationDefaults.find(function(l){return Une(i)===Une(l.mutationKey)});o?o.defaultOptions=r:this.mutationDefaults.push({mutationKey:i,defaultOptions:r})},e.getMutationDefaults=function(i){var r;return i?(r=this.mutationDefaults.find(function(o){return fGe(i,o.mutationKey)}))==null?void 0:r.defaultOptions:void 0},e.defaultQueryOptions=function(i){if(i?._defaulted)return i;var r=Zt({},this.defaultOptions.queries,this.getQueryDefaults(i?.queryKey),i,{_defaulted:!0});return!r.queryHash&&r.queryKey&&(r.queryHash=HUt(r.queryKey,r)),r},e.defaultQueryObserverOptions=function(i){return this.defaultQueryOptions(i)},e.defaultMutationOptions=function(i){return i?._defaulted?i:Zt({},this.defaultOptions.mutations,this.getMutationDefaults(i?.mutationKey),i,{_defaulted:!0})},e.clear=function(){this.queryCache.clear(),this.mutationCache.clear()},n})(),uSr=(function(n){ZW(e,n);function e(i,r){var o;return o=n.call(this)||this,o.client=i,o.options=r,o.trackedProps=[],o.selectError=null,o.bindMethods(),o.setOptions(r),o}var t=e.prototype;return t.bindMethods=function(){this.remove=this.remove.bind(this),this.refetch=this.refetch.bind(this)},t.onSubscribe=function(){this.listeners.length===1&&(this.currentQuery.addObserver(this),T9n(this.currentQuery,this.options)&&this.executeFetch(),this.updateTimers())},t.onUnsubscribe=function(){this.listeners.length||this.destroy()},t.shouldFetchOnReconnect=function(){return cNt(this.currentQuery,this.options,this.options.refetchOnReconnect)},t.shouldFetchOnWindowFocus=function(){return cNt(this.currentQuery,this.options,this.options.refetchOnWindowFocus)},t.destroy=function(){this.listeners=[],this.clearTimers(),this.currentQuery.removeObserver(this)},t.setOptions=function(r,o){var l=this.options,c=this.currentQuery;if(this.options=this.client.defaultQueryObserverOptions(r),typeof this.options.enabled<"u"&&typeof this.options.enabled!="boolean")throw new Error("Expected enabled to be a boolean");this.options.queryKey||(this.options.queryKey=l.queryKey),this.updateQuery();var d=this.hasListeners();d&&L9n(this.currentQuery,c,this.options,l)&&this.executeFetch(),this.updateResult(o),d&&(this.currentQuery!==c||this.options.enabled!==l.enabled||this.options.staleTime!==l.staleTime)&&this.updateStaleTimeout();var h=this.computeRefetchInterval();d&&(this.currentQuery!==c||this.options.enabled!==l.enabled||h!==this.currentRefetchInterval)&&this.updateRefetchInterval(h)},t.getOptimisticResult=function(r){var o=this.client.defaultQueryObserverOptions(r),l=this.client.getQueryCache().build(this.client,o);return this.createResult(l,o)},t.getCurrentResult=function(){return this.currentResult},t.trackResult=function(r,o){var l=this,c={},d=function(p){l.trackedProps.includes(p)||l.trackedProps.push(p)};return Object.keys(r).forEach(function(h){Object.defineProperty(c,h,{configurable:!1,enumerable:!0,get:function(){return d(h),r[h]}})}),(o.useErrorBoundary||o.suspense)&&d("error"),c},t.getNextResult=function(r){var o=this;return new Promise(function(l,c){var d=o.subscribe(function(h){h.isFetching||(d(),h.isError&&r?.throwOnError?c(h.error):l(h))})})},t.getCurrentQuery=function(){return this.currentQuery},t.remove=function(){this.client.getQueryCache().remove(this.currentQuery)},t.refetch=function(r){return this.fetch(Zt({},r,{meta:{refetchPage:r?.refetchPage}}))},t.fetchOptimistic=function(r){var o=this,l=this.client.defaultQueryObserverOptions(r),c=this.client.getQueryCache().build(this.client,l);return c.fetch().then(function(){return o.createResult(c,l)})},t.fetch=function(r){var o=this;return this.executeFetch(r).then(function(){return o.updateResult(),o.currentResult})},t.executeFetch=function(r){this.updateQuery();var o=this.currentQuery.fetch(this.options,r);return r?.throwOnError||(o=o.catch(V2)),o},t.updateStaleTimeout=function(){var r=this;if(this.clearStaleTimeout(),!(dGe||this.currentResult.isStale||!aNt(this.options.staleTime))){var o=vhi(this.currentResult.dataUpdatedAt,this.options.staleTime),l=o+1;this.staleTimeoutId=setTimeout(function(){r.currentResult.isStale||r.updateResult()},l)}},t.computeRefetchInterval=function(){var r;return typeof this.options.refetchInterval=="function"?this.options.refetchInterval(this.currentResult.data,this.currentQuery):(r=this.options.refetchInterval)!=null?r:!1},t.updateRefetchInterval=function(r){var o=this;this.clearRefetchInterval(),this.currentRefetchInterval=r,!(dGe||this.options.enabled===!1||!aNt(this.currentRefetchInterval)||this.currentRefetchInterval===0)&&(this.refetchIntervalId=setInterval(function(){(o.options.refetchIntervalInBackground||d4e.isFocused())&&o.executeFetch()},this.currentRefetchInterval))},t.updateTimers=function(){this.updateStaleTimeout(),this.updateRefetchInterval(this.computeRefetchInterval())},t.clearTimers=function(){this.clearStaleTimeout(),this.clearRefetchInterval()},t.clearStaleTimeout=function(){this.staleTimeoutId&&(clearTimeout(this.staleTimeoutId),this.staleTimeoutId=void 0)},t.clearRefetchInterval=function(){this.refetchIntervalId&&(clearInterval(this.refetchIntervalId),this.refetchIntervalId=void 0)},t.createResult=function(r,o){var l=this.currentQuery,c=this.options,d=this.currentResult,h=this.currentResultState,p=this.currentResultOptions,m=r!==l,b=m?r.state:this.currentQueryInitialState,w=m?this.currentResult:this.previousQueryResult,_=r.state,x=_.dataUpdatedAt,T=_.error,I=_.errorUpdatedAt,L=_.isFetching,A=_.status,M=!1,O=!1,F;if(o.optimisticResults){var j=this.hasListeners(),W=!j&&T9n(r,o),q=j&&L9n(r,l,o,c);(W||q)&&(L=!0,x||(A="loading"))}if(o.keepPreviousData&&!_.dataUpdateCount&&w?.isSuccess&&A!=="error")F=w.data,x=w.dataUpdatedAt,A=w.status,M=!0;else if(o.select&&typeof _.data<"u")if(d&&_.data===h?.data&&o.select===this.selectFn)F=this.selectResult;else try{this.selectFn=o.select,F=o.select(_.data),o.structuralSharing!==!1&&(F=pGe(d?.data,F)),this.selectResult=F,this.selectError=null}catch(G){mGe().error(G),this.selectError=G}else F=_.data;if(typeof o.placeholderData<"u"&&typeof F>"u"&&(A==="loading"||A==="idle")){var Z;if(d?.isPlaceholderData&&o.placeholderData===p?.placeholderData)Z=d.data;else if(Z=typeof o.placeholderData=="function"?o.placeholderData():o.placeholderData,o.select&&typeof Z<"u")try{Z=o.select(Z),o.structuralSharing!==!1&&(Z=pGe(d?.data,Z)),this.selectError=null}catch(G){mGe().error(G),this.selectError=G}typeof Z<"u"&&(A="success",F=Z,O=!0)}this.selectError&&(T=this.selectError,F=this.selectResult,I=Date.now(),A="error");var ee={status:A,isLoading:A==="loading",isSuccess:A==="success",isError:A==="error",isIdle:A==="idle",data:F,dataUpdatedAt:x,error:T,errorUpdatedAt:I,failureCount:_.fetchFailureCount,errorUpdateCount:_.errorUpdateCount,isFetched:_.dataUpdateCount>0||_.errorUpdateCount>0,isFetchedAfterMount:_.dataUpdateCount>b.dataUpdateCount||_.errorUpdateCount>b.errorUpdateCount,isFetching:L,isRefetching:L&&A!=="loading",isLoadingError:A==="error"&&_.dataUpdatedAt===0,isPlaceholderData:O,isPreviousData:M,isRefetchError:A==="error"&&_.dataUpdatedAt!==0,isStale:BUt(r,o),refetch:this.refetch,remove:this.remove};return ee},t.shouldNotifyListeners=function(r,o){if(!o)return!0;var l=this.options,c=l.notifyOnChangeProps,d=l.notifyOnChangePropsExclusions;if(!c&&!d||c==="tracked"&&!this.trackedProps.length)return!0;var h=c==="tracked"?this.trackedProps:c;return Object.keys(r).some(function(p){var m=p,b=r[m]!==o[m],w=h?.some(function(x){return x===p}),_=d?.some(function(x){return x===p});return b&&!_&&(!h||w)})},t.updateResult=function(r){var o=this.currentResult;if(this.currentResult=this.createResult(this.currentQuery,this.options),this.currentResultState=this.currentQuery.state,this.currentResultOptions=this.options,!YCr(this.currentResult,o)){var l={cache:!0};r?.listeners!==!1&&this.shouldNotifyListeners(this.currentResult,o)&&(l.listeners=!0),this.notify(Zt({},l,r))}},t.updateQuery=function(){var r=this.client.getQueryCache().build(this.client,this.options);if(r!==this.currentQuery){var o=this.currentQuery;this.currentQuery=r,this.currentQueryInitialState=r.state,this.previousQueryResult=this.currentResult,this.hasListeners()&&(o?.removeObserver(this),r.addObserver(this))}},t.onQueryUpdate=function(r){var o={};r.type==="success"?o.onSuccess=!0:r.type==="error"&&!SUe(r.error)&&(o.onError=!0),this.updateResult(o),this.hasListeners()&&this.updateTimers()},t.notify=function(r){var o=this;D1.batch(function(){r.onSuccess?(o.options.onSuccess==null||o.options.onSuccess(o.currentResult.data),o.options.onSettled==null||o.options.onSettled(o.currentResult.data,null)):r.onError&&(o.options.onError==null||o.options.onError(o.currentResult.error),o.options.onSettled==null||o.options.onSettled(void 0,o.currentResult.error)),r.listeners&&o.listeners.forEach(function(l){l(o.currentResult)}),r.cache&&o.client.getQueryCache().notify({query:o.currentQuery,type:"observerResultsUpdated"})})},e})(Q1e);function dSr(n,e){return e.enabled!==!1&&!n.state.dataUpdatedAt&&!(n.state.status==="error"&&e.retryOnMount===!1)}function T9n(n,e){return dSr(n,e)||n.state.dataUpdatedAt>0&&cNt(n,e,e.refetchOnMount)}function cNt(n,e,t){if(e.enabled!==!1){var i=typeof t=="function"?t(n):t;return i==="always"||i!==!1&&BUt(n,e)}return!1}function L9n(n,e,t,i){return t.enabled!==!1&&(n!==e||i.enabled===!1)&&(!t.suspense||n.state.status!=="error")&&BUt(n,t)}function BUt(n,e){return n.isStaleByTime(e.staleTime)}var hSr=(function(n){ZW(e,n);function e(i,r){var o;return o=n.call(this)||this,o.client=i,o.setOptions(r),o.bindMethods(),o.updateResult(),o}var t=e.prototype;return t.bindMethods=function(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)},t.setOptions=function(r){this.options=this.client.defaultMutationOptions(r)},t.onUnsubscribe=function(){if(!this.listeners.length){var r;(r=this.currentMutation)==null||r.removeObserver(this)}},t.onMutationUpdate=function(r){this.updateResult();var o={listeners:!0};r.type==="success"?o.onSuccess=!0:r.type==="error"&&(o.onError=!0),this.notify(o)},t.getCurrentResult=function(){return this.currentResult},t.reset=function(){this.currentMutation=void 0,this.updateResult(),this.notify({listeners:!0})},t.mutate=function(r,o){return this.mutateOptions=o,this.currentMutation&&this.currentMutation.removeObserver(this),this.currentMutation=this.client.getMutationCache().build(this.client,Zt({},this.options,{variables:typeof r<"u"?r:this.options.variables})),this.currentMutation.addObserver(this),this.currentMutation.execute()},t.updateResult=function(){var r=this.currentMutation?this.currentMutation.state:xhi(),o=Zt({},r,{isLoading:r.status==="loading",isSuccess:r.status==="success",isError:r.status==="error",isIdle:r.status==="idle",mutate:this.mutate,reset:this.reset});this.currentResult=o},t.notify=function(r){var o=this;D1.batch(function(){o.mutateOptions&&(r.onSuccess?(o.mutateOptions.onSuccess==null||o.mutateOptions.onSuccess(o.currentResult.data,o.currentResult.variables,o.currentResult.context),o.mutateOptions.onSettled==null||o.mutateOptions.onSettled(o.currentResult.data,null,o.currentResult.variables,o.currentResult.context)):r.onError&&(o.mutateOptions.onError==null||o.mutateOptions.onError(o.currentResult.error,o.currentResult.variables,o.currentResult.context),o.mutateOptions.onSettled==null||o.mutateOptions.onSettled(void 0,o.currentResult.error,o.currentResult.variables,o.currentResult.context))),r.listeners&&o.listeners.forEach(function(l){l(o.currentResult)})})},e})(Q1e),fSr=Ffe.unstable_batchedUpdates;D1.setBatchNotifyFunction(fSr);var pSr=console;tSr(pSr);var D9n=Rt.createContext(void 0),Ehi=Rt.createContext(!1);function khi(n){return n&&typeof window<"u"?(window.ReactQueryClientContext||(window.ReactQueryClientContext=D9n),window.ReactQueryClientContext):D9n}var u7=function(){var e=Rt.useContext(khi(Rt.useContext(Ehi)));if(!e)throw new Error("No QueryClient set, use QueryClientProvider to set one");return e},gSr=function(e){var t=e.client,i=e.contextSharing,r=i===void 0?!1:i,o=e.children;Rt.useEffect(function(){return t.mount(),function(){t.unmount()}},[t]);var l=khi(r);return Rt.createElement(Ehi.Provider,{value:r},Rt.createElement(l.Provider,{value:t},o))};function mSr(){var n=!1;return{clearReset:function(){n=!1},reset:function(){n=!0},isReset:function(){return n}}}var bSr=Rt.createContext(mSr()),vSr=function(){return Rt.useContext(bSr)};function Thi(n,e,t){return typeof e=="function"?e.apply(void 0,t):typeof e=="boolean"?e:!!n}function WY(n,e,t){var i=Rt.useRef(!1),r=Rt.useState(0),o=r[1],l=qCr(n,e,t),c=u7(),d=Rt.useRef();d.current?d.current.setOptions(l):d.current=new hSr(c,l);var h=d.current.getCurrentResult();Rt.useEffect(function(){i.current=!0;var m=d.current.subscribe(D1.batchCalls(function(){i.current&&o(function(b){return b+1})}));return function(){i.current=!1,m()}},[]);var p=Rt.useCallback(function(m,b){d.current.mutate(m,b).catch(V2)},[]);if(h.error&&Thi(void 0,d.current.options.useErrorBoundary,[h.error]))throw h.error;return Zt({},h,{mutate:p,mutateAsync:h.mutate})}function wSr(n,e){var t=Rt.useRef(!1),i=Rt.useState(0),r=i[1],o=u7(),l=vSr(),c=o.defaultQueryObserverOptions(n);c.optimisticResults=!0,c.onError&&(c.onError=D1.batchCalls(c.onError)),c.onSuccess&&(c.onSuccess=D1.batchCalls(c.onSuccess)),c.onSettled&&(c.onSettled=D1.batchCalls(c.onSettled)),c.suspense&&(typeof c.staleTime!="number"&&(c.staleTime=1e3),c.cacheTime===0&&(c.cacheTime=1)),(c.suspense||c.useErrorBoundary)&&(l.isReset()||(c.retryOnMount=!1));var d=Rt.useState(function(){return new e(o,c)}),h=d[0],p=h.getOptimisticResult(c);if(Rt.useEffect(function(){t.current=!0,l.clearReset();var m=h.subscribe(D1.batchCalls(function(){t.current&&r(function(b){return b+1})}));return h.updateResult(),function(){t.current=!1,m()}},[l,h]),Rt.useEffect(function(){h.setOptions(c,{listeners:!1})},[c,h]),c.suspense&&p.isLoading)throw h.fetchOptimistic(c).then(function(m){var b=m.data;c.onSuccess==null||c.onSuccess(b),c.onSettled==null||c.onSettled(b,null)}).catch(function(m){l.clearReset(),c.onError==null||c.onError(m),c.onSettled==null||c.onSettled(void 0,m)});if(p.isError&&!l.isReset()&&!p.isFetching&&Thi(c.suspense,c.useErrorBoundary,[p.error,h.getCurrentQuery()]))throw p.error;return c.notifyOnChangeProps==="tracked"&&(p=h.trackResult(p,c)),p}function coe(n,e,t){var i=_Ue(n,e,t);return wSr(i,uSr)}var UEt={exports:{}},I9n;function ySr(){return I9n||(I9n=1,UEt.exports={ReactQueryDevtools:function(){return null},ReactQueryDevtoolsPanel:function(){return null}}),UEt.exports}var _Sr=ySr();var Lhi=n=>{throw TypeError(n)},CSr=(n,e,t)=>e.has(n)||Lhi("Cannot "+t),qEt=(n,e,t)=>(CSr(n,e,"read from private field"),t?t.call(n):e.get(n)),SSr=(n,e,t)=>e.has(n)?Lhi("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(n):e.set(n,t),A9n="popstate";function R9n(n){return typeof n=="object"&&n!=null&&"pathname"in n&&"search"in n&&"hash"in n&&"state"in n&&"key"in n}function xSr(n={}){function e(i,r){let o=r.state?.masked,{pathname:l,search:c,hash:d}=o||i.location;return bLe("",{pathname:l,search:c,hash:d},r.state&&r.state.usr||null,r.state&&r.state.key||"default",o?{pathname:i.location.pathname,search:i.location.search,hash:i.location.hash}:void 0)}function t(i,r){return typeof r=="string"?r:z9(r)}return kSr(e,t,null,n)}function od(n,e){if(n===!1||n===null||typeof n>"u")throw new Error(e)}function pv(n,e){if(!n){typeof console<"u"&&console.warn(e);try{throw new Error(e)}catch{}}}function ESr(){return Math.random().toString(36).substring(2,10)}function M9n(n,e){return{usr:n.state,key:n.key,idx:e,masked:n.unstable_mask?{pathname:n.pathname,search:n.search,hash:n.hash}:void 0}}function bLe(n,e,t=null,i,r){return{pathname:typeof n=="string"?n:n.pathname,search:"",hash:"",...typeof e=="string"?QW(e):e,state:t,key:e&&e.key||i||ESr(),unstable_mask:r}}function z9({pathname:n="/",search:e="",hash:t=""}){return e&&e!=="?"&&(n+=e.charAt(0)==="?"?e:"?"+e),t&&t!=="#"&&(n+=t.charAt(0)==="#"?t:"#"+t),n}function QW(n){let e={};if(n){let t=n.indexOf("#");t>=0&&(e.hash=n.substring(t),n=n.substring(0,t));let i=n.indexOf("?");i>=0&&(e.search=n.substring(i),n=n.substring(0,i)),n&&(e.pathname=n)}return e}function kSr(n,e,t,i={}){let{window:r=document.defaultView,v5Compat:o=!1}=i,l=r.history,c="POP",d=null,h=p();h==null&&(h=0,l.replaceState({...l.state,idx:h},""));function p(){return(l.state||{idx:null}).idx}function m(){c="POP";let T=p(),I=T==null?null:T-h;h=T,d&&d({action:c,location:x.location,delta:I})}function b(T,I){c="PUSH";let L=R9n(T)?T:bLe(x.location,T,I);h=p()+1;let A=M9n(L,h),M=x.createHref(L.unstable_mask||L);try{l.pushState(A,"",M)}catch(O){if(O instanceof DOMException&&O.name==="DataCloneError")throw O;r.location.assign(M)}o&&d&&d({action:c,location:x.location,delta:1})}function w(T,I){c="REPLACE";let L=R9n(T)?T:bLe(x.location,T,I);h=p();let A=M9n(L,h),M=x.createHref(L.unstable_mask||L);l.replaceState(A,"",M),o&&d&&d({action:c,location:x.location,delta:0})}function _(T){return Dhi(T)}let x={get action(){return c},get location(){return n(r,l)},listen(T){if(d)throw new Error("A history only accepts one active listener");return r.addEventListener(A9n,m),d=T,()=>{r.removeEventListener(A9n,m),d=null}},createHref(T){return e(r,T)},createURL:_,encodeLocation(T){let I=_(T);return{pathname:I.pathname,search:I.search,hash:I.hash}},push:b,replace:w,go(T){return l.go(T)}};return x}function Dhi(n,e=!1){let t="http://localhost";typeof window<"u"&&(t=window.location.origin!=="null"?window.location.origin:window.location.href),od(t,"No window.location.(origin|href) available to create URL");let i=typeof n=="string"?n:z9(n);return i=i.replace(/ $/,"%20"),!e&&i.startsWith("//")&&(i=t+i),new URL(i,t)}var gke,O9n=class{constructor(n){if(SSr(this,gke,new Map),n)for(let[e,t]of n)this.set(e,t)}get(n){if(qEt(this,gke).has(n))return qEt(this,gke).get(n);if(n.defaultValue!==void 0)return n.defaultValue;throw new Error("No value found for context")}set(n,e){qEt(this,gke).set(n,e)}};gke=new WeakMap;var TSr=new Set(["lazy","caseSensitive","path","id","index","children"]);function LSr(n){return TSr.has(n)}var DSr=new Set(["lazy","caseSensitive","path","id","index","middleware","children"]);function ISr(n){return DSr.has(n)}function ASr(n){return n.index===!0}function vLe(n,e,t=[],i={},r=!1){return n.map((o,l)=>{let c=[...t,String(l)],d=typeof o.id=="string"?o.id:c.join("-");if(od(o.index!==!0||!o.children,"Cannot specify children on an index route"),od(r||!i[d],`Found a route id collision on id "${d}". Route id's must be globally unique within Data Router usages`),ASr(o)){let h={...o,id:d};return i[d]=N9n(h,e(h)),h}else{let h={...o,id:d,children:void 0};return i[d]=N9n(h,e(h)),o.children&&(h.children=vLe(o.children,e,c,i,r)),h}})}function N9n(n,e){return Object.assign(n,{...e,...typeof e.lazy=="object"&&e.lazy!=null?{lazy:{...n.lazy,...e.lazy}}:{}})}function _G(n,e,t="/"){return mke(n,e,t,!1)}function mke(n,e,t,i){let r=typeof e=="string"?QW(e):e,o=oT(r.pathname||"/",t);if(o==null)return null;let l=Ihi(n);MSr(l);let c=null;for(let d=0;c==null&&d{let p={relativePath:h===void 0?l.path||"":h,caseSensitive:l.caseSensitive===!0,childrenIndex:c,route:l};if(p.relativePath.startsWith("/")){if(!p.relativePath.startsWith(i)&&d)return;od(p.relativePath.startsWith(i),`Absolute route path "${p.relativePath}" nested under path "${i}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),p.relativePath=p.relativePath.slice(i.length)}let m=iM([i,p.relativePath]),b=t.concat(p);l.children&&l.children.length>0&&(od(l.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${m}".`),Ihi(l.children,e,b,m,d)),!(l.path==null&&!l.index)&&e.push({path:m,score:BSr(m,l.index),routesMeta:b})};return n.forEach((l,c)=>{if(l.path===""||!l.path?.includes("?"))o(l,c);else for(let d of Ahi(l.path))o(l,c,!0,d)}),e}function Ahi(n){let e=n.split("/");if(e.length===0)return[];let[t,...i]=e,r=t.endsWith("?"),o=t.replace(/\?$/,"");if(i.length===0)return r?[o,""]:[o];let l=Ahi(i.join("/")),c=[];return c.push(...l.map(d=>d===""?o:[o,d].join("/"))),r&&c.push(...l),c.map(d=>n.startsWith("/")&&d===""?"/":d)}function MSr(n){n.sort((e,t)=>e.score!==t.score?t.score-e.score:WSr(e.routesMeta.map(i=>i.childrenIndex),t.routesMeta.map(i=>i.childrenIndex)))}var OSr=/^:[\w-]+$/,NSr=3,PSr=2,FSr=1,jSr=10,HSr=-2,P9n=n=>n==="*";function BSr(n,e){let t=n.split("/"),i=t.length;return t.some(P9n)&&(i+=HSr),e&&(i+=PSr),t.filter(r=>!P9n(r)).reduce((r,o)=>r+(OSr.test(o)?NSr:o===""?FSr:jSr),i)}function WSr(n,e){return n.length===e.length&&n.slice(0,-1).every((i,r)=>i===e[r])?n[n.length-1]-e[e.length-1]:0}function VSr(n,e,t=!1){let{routesMeta:i}=n,r={},o="/",l=[];for(let c=0;c{if(p==="*"){let _=c[b]||"";l=o.slice(0,o.length-_.length).replace(/(.)\/+$/,"$1")}const w=c[b];return m&&!w?h[p]=void 0:h[p]=(w||"").replace(/%2F/g,"/"),h},{}),pathname:o,pathnameBase:l,pattern:n}}function $Sr(n,e=!1,t=!0){pv(n==="*"||!n.endsWith("*")||n.endsWith("/*"),`Route path "${n}" will be treated as if it were "${n.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${n.replace(/\*$/,"/*")}".`);let i=[],r="^"+n.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(l,c,d,h,p)=>{if(i.push({paramName:c,isOptional:d!=null}),d){let m=p.charAt(h+l.length);return m&&m!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return n.endsWith("*")?(i.push({paramName:"*"}),r+=n==="*"||n==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):t?r+="\\/*$":n!==""&&n!=="/"&&(r+="(?:(?=\\/|$))"),[new RegExp(r,e?void 0:"i"),i]}function zSr(n){try{return n.split("/").map(e=>decodeURIComponent(e).replace(/\//g,"%2F")).join("/")}catch(e){return pv(!1,`The URL path "${n}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${e}).`),n}}function oT(n,e){if(e==="/")return n;if(!n.toLowerCase().startsWith(e.toLowerCase()))return null;let t=e.endsWith("/")?e.length-1:e.length,i=n.charAt(t);return i&&i!=="/"?null:n.slice(t)||"/"}function USr({basename:n,pathname:e}){return e==="/"?n:iM([n,e])}var Rhi=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,WUt=n=>Rhi.test(n);function qSr(n,e="/"){let{pathname:t,search:i="",hash:r=""}=typeof n=="string"?QW(n):n,o;return t?(t=t.replace(/\/\/+/g,"/"),t.startsWith("/")?o=F9n(t.substring(1),"/"):o=F9n(t,e)):o=e,{pathname:o,search:KSr(i),hash:YSr(r)}}function F9n(n,e){let t=e.replace(/\/+$/,"").split("/");return n.split("/").forEach(r=>{r===".."?t.length>1&&t.pop():r!=="."&&t.push(r)}),t.length>1?t.join("/"):"/"}function GEt(n,e,t,i){return`Cannot include a '${n}' character in a manually specified \`to.${e}\` field [${JSON.stringify(i)}]. Please separate it out to the \`to.${t}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function Mhi(n){return n.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function Jet(n){let e=Mhi(n);return e.map((t,i)=>i===e.length-1?t.pathname:t.pathnameBase)}function S5e(n,e,t,i=!1){let r;typeof n=="string"?r=QW(n):(r={...n},od(!r.pathname||!r.pathname.includes("?"),GEt("?","pathname","search",r)),od(!r.pathname||!r.pathname.includes("#"),GEt("#","pathname","hash",r)),od(!r.search||!r.search.includes("#"),GEt("#","search","hash",r)));let o=n===""||r.pathname==="",l=o?"/":r.pathname,c;if(l==null)c=t;else{let m=e.length-1;if(!i&&l.startsWith("..")){let b=l.split("/");for(;b[0]==="..";)b.shift(),m-=1;r.pathname=b.join("/")}c=m>=0?e[m]:"/"}let d=qSr(r,c),h=l&&l!=="/"&&l.endsWith("/"),p=(o||l===".")&&t.endsWith("/");return!d.pathname.endsWith("/")&&(h||p)&&(d.pathname+="/"),d}var iM=n=>n.join("/").replace(/\/\/+/g,"/"),GSr=n=>n.replace(/\/+$/,"").replace(/^\/*/,"/"),KSr=n=>!n||n==="?"?"":n.startsWith("?")?n:"?"+n,YSr=n=>!n||n==="#"?"":n.startsWith("#")?n:"#"+n,x5e=class{constructor(n,e,t,i=!1){this.status=n,this.statusText=e||"",this.internal=i,t instanceof Error?(this.data=t.toString(),this.error=t):this.data=t}};function wLe(n){return n!=null&&typeof n.status=="number"&&typeof n.statusText=="string"&&typeof n.internal=="boolean"&&"data"in n}function E5e(n){return n.map(e=>e.route.path).filter(Boolean).join("/").replace(/\/\/*/g,"/")||"/"}var Ohi=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Nhi(n,e){let t=n;if(typeof t!="string"||!Rhi.test(t))return{absoluteURL:void 0,isExternal:!1,to:t};let i=t,r=!1;if(Ohi)try{let o=new URL(window.location.href),l=t.startsWith("//")?new URL(o.protocol+t):new URL(t),c=oT(l.pathname,e);l.origin===o.origin&&c!=null?t=c+l.search+l.hash:r=!0}catch{pv(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:i,isExternal:r,to:t}}var jG=Symbol("Uninstrumented");function ZSr(n,e){let t={lazy:[],"lazy.loader":[],"lazy.action":[],"lazy.middleware":[],middleware:[],loader:[],action:[]};n.forEach(r=>r({id:e.id,index:e.index,path:e.path,instrument(o){let l=Object.keys(t);for(let c of l)o[c]&&t[c].push(o[c])}}));let i={};if(typeof e.lazy=="function"&&t.lazy.length>0){let r=Vfe(t.lazy,e.lazy,()=>{});r&&(i.lazy=r)}if(typeof e.lazy=="object"){let r=e.lazy;["middleware","loader","action"].forEach(o=>{let l=r[o],c=t[`lazy.${o}`];if(typeof l=="function"&&c.length>0){let d=Vfe(c,l,()=>{});d&&(i.lazy=Object.assign(i.lazy||{},{[o]:d}))}})}return["loader","action"].forEach(r=>{let o=e[r];if(typeof o=="function"&&t[r].length>0){let l=o[jG]??o,c=Vfe(t[r],l,(...d)=>j9n(d[0]));c&&(r==="loader"&&l.hydrate===!0&&(c.hydrate=!0),c[jG]=l,i[r]=c)}}),e.middleware&&e.middleware.length>0&&t.middleware.length>0&&(i.middleware=e.middleware.map(r=>{let o=r[jG]??r,l=Vfe(t.middleware,o,(...c)=>j9n(c[0]));return l?(l[jG]=o,l):r})),i}function XSr(n,e){let t={navigate:[],fetch:[]};if(e.forEach(i=>i({instrument(r){let o=Object.keys(r);for(let l of o)r[l]&&t[l].push(r[l])}})),t.navigate.length>0){let i=n.navigate[jG]??n.navigate,r=Vfe(t.navigate,i,(...o)=>{let[l,c]=o;return{to:typeof l=="number"||typeof l=="string"?l:l?z9(l):".",...H9n(n,c??{})}});r&&(r[jG]=i,n.navigate=r)}if(t.fetch.length>0){let i=n.fetch[jG]??n.fetch,r=Vfe(t.fetch,i,(...o)=>{let[l,,c,d]=o;return{href:c??".",fetcherKey:l,...H9n(n,d??{})}});r&&(r[jG]=i,n.fetch=r)}return n}function Vfe(n,e,t){return n.length===0?null:async(...i)=>{let r=await Phi(n,t(...i),()=>e(...i),n.length-1);if(r.type==="error")throw r.value;return r.value}}async function Phi(n,e,t,i){let r=n[i],o;if(r){let l,c=async()=>(l?console.error("You cannot call instrumented handlers more than once"):l=Phi(n,e,t,i-1),o=await l,od(o,"Expected a result"),o.type==="error"&&o.value instanceof Error?{status:"error",error:o.value}:{status:"success",error:void 0});try{await r(c,e)}catch(d){console.error("An instrumentation function threw an error:",d)}l||await c(),await l}else try{o={type:"success",value:await t()}}catch(l){o={type:"error",value:l}}return o||{type:"error",value:new Error("No result assigned in instrumentation chain.")}}function j9n(n){let{request:e,context:t,params:i,unstable_pattern:r}=n;return{request:QSr(e),params:{...i},unstable_pattern:r,context:JSr(t)}}function H9n(n,e){return{currentUrl:z9(n.state.location),..."formMethod"in e?{formMethod:e.formMethod}:{},..."formEncType"in e?{formEncType:e.formEncType}:{},..."formData"in e?{formData:e.formData}:{},..."body"in e?{body:e.body}:{}}}function QSr(n){return{method:n.method,url:n.url,headers:{get:(...e)=>n.headers.get(...e)}}}function JSr(n){if(txr(n)){let e={...n};return Object.freeze(e),e}else return{get:e=>n.get(e)}}var exr=Object.getOwnPropertyNames(Object.prototype).sort().join("\0");function txr(n){if(n===null||typeof n!="object")return!1;const e=Object.getPrototypeOf(n);return e===Object.prototype||e===null||Object.getOwnPropertyNames(e).sort().join("\0")===exr}var Fhi=["POST","PUT","PATCH","DELETE"],nxr=new Set(Fhi),ixr=["GET",...Fhi],rxr=new Set(ixr),jhi=new Set([301,302,303,307,308]),sxr=new Set([307,308]),KEt={state:"idle",location:void 0,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0},oxr={state:"idle",data:void 0,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0},bfe={state:"unblocked",proceed:void 0,reset:void 0,location:void 0},axr=n=>({hasErrorBoundary:!!n.hasErrorBoundary}),Hhi="remix-router-transitions",Bhi=Symbol("ResetLoaderData");function lxr(n){const e=n.window?n.window:typeof window<"u"?window:void 0,t=typeof e<"u"&&typeof e.document<"u"&&typeof e.document.createElement<"u";od(n.routes.length>0,"You must provide a non-empty routes array to createRouter");let i=n.hydrationRouteProperties||[],r=n.mapRouteProperties||axr,o=r;if(n.unstable_instrumentations){let Ft=n.unstable_instrumentations;o=rn=>({...r(rn),...ZSr(Ft.map(zn=>zn.route).filter(Boolean),rn)})}let l={},c=vLe(n.routes,o,void 0,l),d,h=n.basename||"/";h.startsWith("/")||(h=`/${h}`);let p=n.dataStrategy||fxr,m={...n.future},b=null,w=new Set,_=null,x=null,T=null,I=n.hydrationData!=null,L=_G(c,n.history.location,h),A=!1,M=null,O,F;if(L==null&&!n.patchRoutesOnNavigation){let Ft=ZD(404,{pathname:n.history.location.pathname}),{matches:rn,route:zn}=GBe(c);O=!0,F=!O,L=rn,M={[zn.id]:Ft}}else if(L&&!n.hydrationData&&Ls(L,c,n.history.location.pathname).active&&(L=null),L)if(L.some(Ft=>Ft.route.lazy))O=!1,F=!O;else if(!L.some(Ft=>VUt(Ft.route)))O=!0,F=!O;else{let Ft=n.hydrationData?n.hydrationData.loaderData:null,rn=n.hydrationData?n.hydrationData.errors:null,zn=L;if(rn){let Oi=L.findIndex(Ki=>rn[Ki.route.id]!==void 0);zn=zn.slice(0,Oi+1)}F=!1,O=zn.every(Oi=>{let Ki=Whi(Oi.route,Ft,rn);return F=F||Ki.renderFallback,!Ki.shouldLoad})}else{O=!1,F=!O,L=[];let Ft=Ls(null,c,n.history.location.pathname);Ft.active&&Ft.matches&&(A=!0,L=Ft.matches)}let j,W={historyAction:n.history.action,location:n.history.location,matches:L,initialized:O,renderFallback:F,navigation:KEt,restoreScrollPosition:n.hydrationData!=null?!1:null,preventScrollReset:!1,revalidation:"idle",loaderData:n.hydrationData&&n.hydrationData.loaderData||{},actionData:n.hydrationData&&n.hydrationData.actionData||null,errors:n.hydrationData&&n.hydrationData.errors||M,fetchers:new Map,blockers:new Map},q="POP",Z=null,ee=!1,G,te=!1,Q=new Map,ie=null,se=!1,de=!1,ne=new Set,we=new Map,ue=0,ce=-1,ye=new Map,he=new Set,pe=new Map,me=new Map,be=new Set,xe=new Map,Te,Ge=null;function tt(){if(b=n.history.listen(({action:Ft,location:rn,delta:zn})=>{if(Te){Te(),Te=void 0;return}pv(xe.size===0||zn!=null,"You are trying to use a blocker on a POP navigation to a location that was not created by @remix-run/router. This will fail silently in production. This can happen if you are navigating outside the router via `window.history.pushState`/`window.location.hash` instead of using router navigation APIs. This can also happen if you are using createHashRouter and the user manually changes the URL.");let Oi=_r({currentLocation:W.location,nextLocation:rn,historyAction:Ft});if(Oi&&zn!=null){let Ki=new Promise(gs=>{Te=gs});n.history.go(zn*-1),us(Oi,{state:"blocked",location:rn,proceed(){us(Oi,{state:"proceeding",proceed:void 0,reset:void 0,location:rn}),Ki.then(()=>n.history.go(zn))},reset(){let gs=new Map(W.blockers);gs.set(Oi,bfe),He({blockers:gs})}}),Z?.resolve(),Z=null;return}return lt(Ft,rn)}),t){Axr(e,Q);let Ft=()=>Rxr(e,Q);e.addEventListener("pagehide",Ft),ie=()=>e.removeEventListener("pagehide",Ft)}return W.initialized||lt("POP",W.location,{initialHydration:!0}),j}function Ue(){b&&b(),ie&&ie(),w.clear(),G&&G.abort(),W.fetchers.forEach((Ft,rn)=>$i(rn)),W.blockers.forEach((Ft,rn)=>ss(rn))}function Me(Ft){return w.add(Ft),()=>w.delete(Ft)}function He(Ft,rn={}){Ft.matches&&(Ft.matches=Ft.matches.map(Ki=>{let gs=l[Ki.route.id],ur=Ki.route;return ur.element!==gs.element||ur.errorElement!==gs.errorElement||ur.hydrateFallbackElement!==gs.hydrateFallbackElement?{...Ki,route:gs}:Ki})),W={...W,...Ft};let zn=[],Oi=[];W.fetchers.forEach((Ki,gs)=>{Ki.state==="idle"&&(be.has(gs)?zn.push(gs):Oi.push(gs))}),be.forEach(Ki=>{!W.fetchers.has(Ki)&&!we.has(Ki)&&zn.push(Ki)}),[...w].forEach(Ki=>Ki(W,{deletedFetchers:zn,newErrors:Ft.errors??null,viewTransitionOpts:rn.viewTransitionOpts,flushSync:rn.flushSync===!0})),zn.forEach(Ki=>$i(Ki)),Oi.forEach(Ki=>W.fetchers.delete(Ki))}function at(Ft,rn,{flushSync:zn}={}){let Oi=W.actionData!=null&&W.navigation.formMethod!=null&&fS(W.navigation.formMethod)&&W.navigation.state==="loading"&&Ft.state?._isRedirect!==!0,Ki;rn.actionData?Object.keys(rn.actionData).length>0?Ki=rn.actionData:Ki=null:Oi?Ki=W.actionData:Ki=null;let gs=rn.loaderData?Y9n(W.loaderData,rn.loaderData,rn.matches||[],rn.errors):W.loaderData,ur=W.blockers;ur.size>0&&(ur=new Map(ur),ur.forEach((Do,zs)=>ur.set(zs,bfe)));let vs=se?!1:Ri(Ft,rn.matches||W.matches),Ir=ee===!0||W.navigation.formMethod!=null&&fS(W.navigation.formMethod)&&Ft.state?._isRedirect!==!0;d&&(c=d,d=void 0),se||q==="POP"||(q==="PUSH"?n.history.push(Ft,Ft.state):q==="REPLACE"&&n.history.replace(Ft,Ft.state));let oo;if(q==="POP"){let Do=Q.get(W.location.pathname);Do&&Do.has(Ft.pathname)?oo={currentLocation:W.location,nextLocation:Ft}:Q.has(Ft.pathname)&&(oo={currentLocation:Ft,nextLocation:W.location})}else if(te){let Do=Q.get(W.location.pathname);Do?Do.add(Ft.pathname):(Do=new Set([Ft.pathname]),Q.set(W.location.pathname,Do)),oo={currentLocation:W.location,nextLocation:Ft}}He({...rn,actionData:Ki,loaderData:gs,historyAction:q,location:Ft,initialized:!0,renderFallback:!1,navigation:KEt,revalidation:"idle",restoreScrollPosition:vs,preventScrollReset:Ir,blockers:ur},{viewTransitionOpts:oo,flushSync:zn===!0}),q="POP",ee=!1,te=!1,se=!1,de=!1,Z?.resolve(),Z=null,Ge?.resolve(),Ge=null}async function rt(Ft,rn){if(Z?.resolve(),Z=null,typeof Ft=="number"){Z||(Z=J9n());let _n=Z.promise;return n.history.go(Ft),_n}let zn=uNt(W.location,W.matches,h,Ft,rn?.fromRouteId,rn?.relative),{path:Oi,submission:Ki,error:gs}=B9n(!1,zn,rn),ur;rn?.unstable_mask&&(ur={pathname:"",search:"",hash:"",...typeof rn.unstable_mask=="string"?QW(rn.unstable_mask):{...W.location.unstable_mask,...rn.unstable_mask}});let vs=W.location,Ir=bLe(vs,Oi,rn&&rn.state,void 0,ur);Ir={...Ir,...n.history.encodeLocation(Ir)};let oo=rn&&rn.replace!=null?rn.replace:void 0,Do="PUSH";oo===!0?Do="REPLACE":oo===!1||Ki!=null&&fS(Ki.formMethod)&&Ki.formAction===W.location.pathname+W.location.search&&(Do="REPLACE");let zs=rn&&"preventScrollReset"in rn?rn.preventScrollReset===!0:void 0,cn=(rn&&rn.flushSync)===!0,bt=_r({currentLocation:vs,nextLocation:Ir,historyAction:Do});if(bt){us(bt,{state:"blocked",location:Ir,proceed(){us(bt,{state:"proceeding",proceed:void 0,reset:void 0,location:Ir}),rt(Ft,rn)},reset(){let _n=new Map(W.blockers);_n.set(bt,bfe),He({blockers:_n})}});return}await lt(Do,Ir,{submission:Ki,pendingError:gs,preventScrollReset:zs,replace:rn&&rn.replace,enableViewTransition:rn&&rn.viewTransition,flushSync:cn,callSiteDefaultShouldRevalidate:rn&&rn.unstable_defaultShouldRevalidate})}function Be(){Ge||(Ge=J9n()),zt(),He({revalidation:"loading"});let Ft=Ge.promise;return W.navigation.state==="submitting"?Ft:W.navigation.state==="idle"?(lt(W.historyAction,W.location,{startUninterruptedRevalidation:!0}),Ft):(lt(q||W.historyAction,W.navigation.location,{overrideNavigation:W.navigation,enableViewTransition:te===!0}),Ft)}async function lt(Ft,rn,zn){G&&G.abort(),G=null,q=Ft,se=(zn&&zn.startUninterruptedRevalidation)===!0,eo(W.location,W.matches),ee=(zn&&zn.preventScrollReset)===!0,te=(zn&&zn.enableViewTransition)===!0;let Oi=d||c,Ki=zn&&zn.overrideNavigation,gs=zn?.initialHydration&&W.matches&&W.matches.length>0&&!A?W.matches:_G(Oi,rn,h),ur=(zn&&zn.flushSync)===!0;if(gs&&W.initialized&&!de&&_xr(W.location,rn)&&!(zn&&zn.submission&&fS(zn.submission.formMethod))){at(rn,{matches:gs},{flushSync:ur});return}let vs=Ls(gs,Oi,rn.pathname);if(vs.active&&vs.matches&&(gs=vs.matches),!gs){let{error:Di,notFoundMatches:Ni,route:Ds}=uo(rn.pathname);at(rn,{matches:Ni,loaderData:{},errors:{[Ds.id]:Di}},{flushSync:ur});return}G=new AbortController;let Ir=vfe(n.history,rn,G.signal,zn&&zn.submission),oo=n.getContext?await n.getContext():new O9n,Do;if(zn&&zn.pendingError)Do=[CG(gs).route.id,{type:"error",error:zn.pendingError}];else if(zn&&zn.submission&&fS(zn.submission.formMethod)){let Di=await ct(Ir,rn,zn.submission,gs,oo,vs.active,zn&&zn.initialHydration===!0,{replace:zn.replace,flushSync:ur});if(Di.shortCircuited)return;if(Di.pendingActionResult){let[Ni,Ds]=Di.pendingActionResult;if(w6(Ds)&&wLe(Ds.error)&&Ds.error.status===404){G=null,at(rn,{matches:Di.matches,loaderData:{},errors:{[Ni]:Ds.error}});return}}gs=Di.matches||gs,Do=Di.pendingActionResult,Ki=YEt(rn,zn.submission),ur=!1,vs.active=!1,Ir=vfe(n.history,Ir.url,Ir.signal)}let{shortCircuited:zs,matches:cn,loaderData:bt,errors:_n}=await ze(Ir,rn,gs,oo,vs.active,Ki,zn&&zn.submission,zn&&zn.fetcherSubmission,zn&&zn.replace,zn&&zn.initialHydration===!0,ur,Do,zn&&zn.callSiteDefaultShouldRevalidate);zs||(G=null,at(rn,{matches:cn||gs,...Z9n(Do),loaderData:bt,errors:_n}))}async function ct(Ft,rn,zn,Oi,Ki,gs,ur,vs={}){zt();let Ir=Dxr(rn,zn);if(He({navigation:Ir},{flushSync:vs.flushSync===!0}),gs){let zs=await Cr(Oi,rn.pathname,Ft.signal);if(zs.type==="aborted")return{shortCircuited:!0};if(zs.type==="error"){if(zs.partialMatches.length===0){let{matches:bt,route:_n}=GBe(c);return{matches:bt,pendingActionResult:[_n.id,{type:"error",error:zs.error}]}}let cn=CG(zs.partialMatches).route.id;return{matches:zs.partialMatches,pendingActionResult:[cn,{type:"error",error:zs.error}]}}else if(zs.matches)Oi=zs.matches;else{let{notFoundMatches:cn,error:bt,route:_n}=uo(rn.pathname);return{matches:cn,pendingActionResult:[_n.id,{type:"error",error:bt}]}}}let oo,Do=xUe(Oi,rn);if(!Do.route.action&&!Do.route.lazy)oo={type:"error",error:ZD(405,{method:Ft.method,pathname:rn.pathname,routeId:Do.route.id})};else{let zs=ege(o,l,Ft,Oi,Do,ur?[]:i,Ki),cn=await Ye(Ft,zs,Ki,null);if(oo=cn[Do.route.id],!oo){for(let bt of Oi)if(cn[bt.route.id]){oo=cn[bt.route.id];break}}if(Ft.signal.aborted)return{shortCircuited:!0}}if(qne(oo)){let zs;return vs&&vs.replace!=null?zs=vs.replace:zs=q9n(oo.response.headers.get("Location"),new URL(Ft.url),h,n.history)===W.location.pathname+W.location.search,await Ct(Ft,oo,!0,{submission:zn,replace:zs}),{shortCircuited:!0}}if(w6(oo)){let zs=CG(Oi,Do.route.id);return(vs&&vs.replace)!==!0&&(q="PUSH"),{matches:Oi,pendingActionResult:[zs.route.id,oo,Do.route.id]}}return{matches:Oi,pendingActionResult:[Do.route.id,oo]}}async function ze(Ft,rn,zn,Oi,Ki,gs,ur,vs,Ir,oo,Do,zs,cn){let bt=gs||YEt(rn,ur),_n=ur||vs||Q9n(bt),Di=!se&&!oo;if(Ki){if(Di){let Lp=Ke(zs);He({navigation:bt,...Lp!==void 0?{actionData:Lp}:{}},{flushSync:Do})}let Io=await Cr(zn,rn.pathname,Ft.signal);if(Io.type==="aborted")return{shortCircuited:!0};if(Io.type==="error"){if(Io.partialMatches.length===0){let{matches:ud,route:Rg}=GBe(c);return{matches:ud,loaderData:{},errors:{[Rg.id]:Io.error}}}let Lp=CG(Io.partialMatches).route.id;return{matches:Io.partialMatches,loaderData:{},errors:{[Lp]:Io.error}}}else if(Io.matches)zn=Io.matches;else{let{error:Lp,notFoundMatches:ud,route:Rg}=uo(rn.pathname);return{matches:ud,loaderData:{},errors:{[Rg.id]:Lp}}}}let Ni=d||c,{dsMatches:Ds,revalidatingFetchers:Es}=W9n(Ft,Oi,o,l,n.history,W,zn,_n,rn,oo?[]:i,oo===!0,de,ne,be,pe,he,Ni,h,n.patchRoutesOnNavigation!=null,zs,cn);if(ce=++ue,!n.dataStrategy&&!Ds.some(Io=>Io.shouldLoad)&&!Ds.some(Io=>Io.route.middleware&&Io.route.middleware.length>0)&&Es.length===0){let Io=xt();return at(rn,{matches:zn,loaderData:{},errors:zs&&w6(zs[1])?{[zs[0]]:zs[1].error}:null,...Z9n(zs),...Io?{fetchers:new Map(W.fetchers)}:{}},{flushSync:Do}),{shortCircuited:!0}}if(Di){let Io={};if(!Ki){Io.navigation=bt;let Lp=Ke(zs);Lp!==void 0&&(Io.actionData=Lp)}Es.length>0&&(Io.fetchers=$e(Es)),He(Io,{flushSync:Do})}Es.forEach(Io=>{ps(Io.key),Io.controller&&we.set(Io.key,Io.controller)});let $a=()=>Es.forEach(Io=>ps(Io.key));G&&G.signal.addEventListener("abort",$a);let{loaderResults:Lu,fetcherResults:tu}=await wt(Ds,Es,Ft,Oi);if(Ft.signal.aborted)return{shortCircuited:!0};G&&G.signal.removeEventListener("abort",$a),Es.forEach(Io=>we.delete(Io.key));let Kd=KBe(Lu);if(Kd)return await Ct(Ft,Kd.result,!0,{replace:Ir}),{shortCircuited:!0};if(Kd=KBe(tu),Kd)return he.add(Kd.key),await Ct(Ft,Kd.result,!0,{replace:Ir}),{shortCircuited:!0};let{loaderData:ld,errors:Ll}=K9n(W,zn,Lu,zs,Es,tu);oo&&W.errors&&(Ll={...W.errors,...Ll});let Pu=xt(),cd=Ei(ce),Ag=Pu||cd||Es.length>0;return{matches:zn,loaderData:ld,errors:Ll,...Ag?{fetchers:new Map(W.fetchers)}:{}}}function Ke(Ft){if(Ft&&!w6(Ft[1]))return{[Ft[0]]:Ft[1].data};if(W.actionData)return Object.keys(W.actionData).length===0?null:W.actionData}function $e(Ft){return Ft.forEach(rn=>{let zn=W.fetchers.get(rn.key),Oi=Vxe(void 0,zn?zn.data:void 0);W.fetchers.set(rn.key,Oi)}),new Map(W.fetchers)}async function nt(Ft,rn,zn,Oi){ps(Ft);let Ki=(Oi&&Oi.flushSync)===!0,gs=d||c,ur=uNt(W.location,W.matches,h,zn,rn,Oi?.relative),vs=_G(gs,ur,h),Ir=Ls(vs,gs,ur);if(Ir.active&&Ir.matches&&(vs=Ir.matches),!vs){xn(Ft,rn,ZD(404,{pathname:ur}),{flushSync:Ki});return}let{path:oo,submission:Do,error:zs}=B9n(!0,ur,Oi);if(zs){xn(Ft,rn,zs,{flushSync:Ki});return}let cn=n.getContext?await n.getContext():new O9n,bt=(Oi&&Oi.preventScrollReset)===!0;if(Do&&fS(Do.formMethod)){await vt(Ft,rn,oo,vs,cn,Ir.active,Ki,bt,Do,Oi&&Oi.unstable_defaultShouldRevalidate);return}pe.set(Ft,{routeId:rn,path:oo}),await Pt(Ft,rn,oo,vs,cn,Ir.active,Ki,bt,Do)}async function vt(Ft,rn,zn,Oi,Ki,gs,ur,vs,Ir,oo){zt(),pe.delete(Ft);let Do=W.fetchers.get(Ft);mn(Ft,Ixr(Ir,Do),{flushSync:ur});let zs=new AbortController,cn=vfe(n.history,zn,zs.signal,Ir);if(gs){let js=await Cr(Oi,new URL(cn.url).pathname,cn.signal,Ft);if(js.type==="aborted")return;if(js.type==="error"){xn(Ft,rn,js.error,{flushSync:ur});return}else if(js.matches)Oi=js.matches;else{xn(Ft,rn,ZD(404,{pathname:zn}),{flushSync:ur});return}}let bt=xUe(Oi,zn);if(!bt.route.action&&!bt.route.lazy){let js=ZD(405,{method:Ir.formMethod,pathname:zn,routeId:rn});xn(Ft,rn,js,{flushSync:ur});return}we.set(Ft,zs);let _n=ue,Di=ege(o,l,cn,Oi,bt,i,Ki),Ni=await Ye(cn,Di,Ki,Ft),Ds=Ni[bt.route.id];if(!Ds){for(let js of Di)if(Ni[js.route.id]){Ds=Ni[js.route.id];break}}if(cn.signal.aborted){we.get(Ft)===zs&&we.delete(Ft);return}if(be.has(Ft)){if(qne(Ds)||w6(Ds)){mn(Ft,YH(void 0));return}}else{if(qne(Ds))if(we.delete(Ft),ce>_n){mn(Ft,YH(void 0));return}else return he.add(Ft),mn(Ft,Vxe(Ir)),Ct(cn,Ds,!1,{fetcherSubmission:Ir,preventScrollReset:vs});if(w6(Ds)){xn(Ft,rn,Ds.error);return}}let Es=W.navigation.location||W.location,$a=vfe(n.history,Es,zs.signal),Lu=d||c,tu=W.navigation.state!=="idle"?_G(Lu,W.navigation.location,h):W.matches;od(tu,"Didn't find any matches after fetcher action");let Kd=++ue;ye.set(Ft,Kd);let ld=Vxe(Ir,Ds.data);W.fetchers.set(Ft,ld);let{dsMatches:Ll,revalidatingFetchers:Pu}=W9n($a,Ki,o,l,n.history,W,tu,Ir,Es,i,!1,de,ne,be,pe,he,Lu,h,n.patchRoutesOnNavigation!=null,[bt.route.id,Ds],oo);Pu.filter(js=>js.key!==Ft).forEach(js=>{let Pl=js.key,hh=W.fetchers.get(Pl),uf=Vxe(void 0,hh?hh.data:void 0);W.fetchers.set(Pl,uf),ps(Pl),js.controller&&we.set(Pl,js.controller)}),He({fetchers:new Map(W.fetchers)});let cd=()=>Pu.forEach(js=>ps(js.key));zs.signal.addEventListener("abort",cd);let{loaderResults:Ag,fetcherResults:Io}=await wt(Ll,Pu,$a,Ki);if(zs.signal.aborted)return;if(zs.signal.removeEventListener("abort",cd),ye.delete(Ft),we.delete(Ft),Pu.forEach(js=>we.delete(js.key)),W.fetchers.has(Ft)){let js=YH(Ds.data);W.fetchers.set(Ft,js)}let Lp=KBe(Ag);if(Lp)return Ct($a,Lp.result,!1,{preventScrollReset:vs});if(Lp=KBe(Io),Lp)return he.add(Lp.key),Ct($a,Lp.result,!1,{preventScrollReset:vs});let{loaderData:ud,errors:Rg}=K9n(W,tu,Ag,void 0,Pu,Io);Ei(Kd),W.navigation.state==="loading"&&Kd>ce?(od(q,"Expected pending action"),G&&G.abort(),at(W.navigation.location,{matches:tu,loaderData:ud,errors:Rg,fetchers:new Map(W.fetchers)})):(He({errors:Rg,loaderData:Y9n(W.loaderData,ud,tu,Rg),fetchers:new Map(W.fetchers)}),de=!1)}async function Pt(Ft,rn,zn,Oi,Ki,gs,ur,vs,Ir){let oo=W.fetchers.get(Ft);mn(Ft,Vxe(Ir,oo?oo.data:void 0),{flushSync:ur});let Do=new AbortController,zs=vfe(n.history,zn,Do.signal);if(gs){let Ds=await Cr(Oi,new URL(zs.url).pathname,zs.signal,Ft);if(Ds.type==="aborted")return;if(Ds.type==="error"){xn(Ft,rn,Ds.error,{flushSync:ur});return}else if(Ds.matches)Oi=Ds.matches;else{xn(Ft,rn,ZD(404,{pathname:zn}),{flushSync:ur});return}}let cn=xUe(Oi,zn);we.set(Ft,Do);let bt=ue,_n=ege(o,l,zs,Oi,cn,i,Ki),Ni=(await Ye(zs,_n,Ki,Ft))[cn.route.id];if(we.get(Ft)===Do&&we.delete(Ft),!zs.signal.aborted){if(be.has(Ft)){mn(Ft,YH(void 0));return}if(qne(Ni))if(ce>bt){mn(Ft,YH(void 0));return}else{he.add(Ft),await Ct(zs,Ni,!1,{preventScrollReset:vs});return}if(w6(Ni)){xn(Ft,rn,Ni.error);return}mn(Ft,YH(Ni.data))}}async function Ct(Ft,rn,zn,{submission:Oi,fetcherSubmission:Ki,preventScrollReset:gs,replace:ur}={}){zn||(Z?.resolve(),Z=null),rn.response.headers.has("X-Remix-Revalidate")&&(de=!0);let vs=rn.response.headers.get("Location");od(vs,"Expected a Location header on the redirect Response"),vs=q9n(vs,new URL(Ft.url),h,n.history);let Ir=bLe(W.location,vs,{_isRedirect:!0});if(t){let _n=!1;if(rn.response.headers.has("X-Remix-Reload-Document"))_n=!0;else if(WUt(vs)){const Di=Dhi(vs,!0);_n=Di.origin!==e.location.origin||oT(Di.pathname,h)==null}if(_n){ur?e.location.replace(vs):e.location.assign(vs);return}}G=null;let oo=ur===!0||rn.response.headers.has("X-Remix-Replace")?"REPLACE":"PUSH",{formMethod:Do,formAction:zs,formEncType:cn}=W.navigation;!Oi&&!Ki&&Do&&zs&&cn&&(Oi=Q9n(W.navigation));let bt=Oi||Ki;if(sxr.has(rn.response.status)&&bt&&fS(bt.formMethod))await lt(oo,Ir,{submission:{...bt,formAction:vs},preventScrollReset:gs||ee,enableViewTransition:zn?te:void 0});else{let _n=YEt(Ir,Oi);await lt(oo,Ir,{overrideNavigation:_n,fetcherSubmission:Ki,preventScrollReset:gs||ee,enableViewTransition:zn?te:void 0})}}async function Ye(Ft,rn,zn,Oi){let Ki,gs={};try{Ki=await gxr(p,Ft,rn,Oi,zn,!1)}catch(ur){return rn.filter(vs=>vs.shouldLoad).forEach(vs=>{gs[vs.route.id]={type:"error",error:ur}}),gs}if(Ft.signal.aborted)return gs;if(!fS(Ft.method))for(let ur of rn){if(Ki[ur.route.id]?.type==="error")break;!Ki.hasOwnProperty(ur.route.id)&&!W.loaderData.hasOwnProperty(ur.route.id)&&(!W.errors||!W.errors.hasOwnProperty(ur.route.id))&&ur.shouldCallHandler()&&(Ki[ur.route.id]={type:"error",result:new Error(`No result returned from dataStrategy for route ${ur.route.id}`)})}for(let[ur,vs]of Object.entries(Ki))if(Exr(vs)){let Ir=vs.result;gs[ur]={type:"redirect",response:wxr(Ir,Ft,ur,rn,h)}}else gs[ur]=await vxr(vs);return gs}async function wt(Ft,rn,zn,Oi){let Ki=Ye(zn,Ft,Oi,null),gs=Promise.all(rn.map(async Ir=>{if(Ir.matches&&Ir.match&&Ir.request&&Ir.controller){let Do=(await Ye(Ir.request,Ir.matches,Oi,Ir.key))[Ir.match.route.id];return{[Ir.key]:Do}}else return Promise.resolve({[Ir.key]:{type:"error",error:ZD(404,{pathname:Ir.path})}})})),ur=await Ki,vs=(await gs).reduce((Ir,oo)=>Object.assign(Ir,oo),{});return{loaderResults:ur,fetcherResults:vs}}function zt(){de=!0,pe.forEach((Ft,rn)=>{we.has(rn)&&ne.add(rn),ps(rn)})}function mn(Ft,rn,zn={}){W.fetchers.set(Ft,rn),He({fetchers:new Map(W.fetchers)},{flushSync:(zn&&zn.flushSync)===!0})}function xn(Ft,rn,zn,Oi={}){let Ki=CG(W.matches,rn);$i(Ft),He({errors:{[Ki.route.id]:zn},fetchers:new Map(W.fetchers)},{flushSync:(Oi&&Oi.flushSync)===!0})}function hn(Ft){return me.set(Ft,(me.get(Ft)||0)+1),be.has(Ft)&&be.delete(Ft),W.fetchers.get(Ft)||oxr}function Zi(Ft,rn){ps(Ft,rn?.reason),mn(Ft,YH(null))}function $i(Ft){let rn=W.fetchers.get(Ft);we.has(Ft)&&!(rn&&rn.state==="loading"&&ye.has(Ft))&&ps(Ft),pe.delete(Ft),ye.delete(Ft),he.delete(Ft),be.delete(Ft),ne.delete(Ft),W.fetchers.delete(Ft)}function Dr(Ft){let rn=(me.get(Ft)||0)-1;rn<=0?(me.delete(Ft),be.add(Ft)):me.set(Ft,rn),He({fetchers:new Map(W.fetchers)})}function ps(Ft,rn){let zn=we.get(Ft);zn&&(zn.abort(rn),we.delete(Ft))}function nn(Ft){for(let rn of Ft){let zn=hn(rn),Oi=YH(zn.data);W.fetchers.set(rn,Oi)}}function xt(){let Ft=[],rn=!1;for(let zn of he){let Oi=W.fetchers.get(zn);od(Oi,`Expected fetcher: ${zn}`),Oi.state==="loading"&&(he.delete(zn),Ft.push(zn),rn=!0)}return nn(Ft),rn}function Ei(Ft){let rn=[];for(let[zn,Oi]of ye)if(Oi0}function gr(Ft,rn){let zn=W.blockers.get(Ft)||bfe;return xe.get(Ft)!==rn&&xe.set(Ft,rn),zn}function ss(Ft){W.blockers.delete(Ft),xe.delete(Ft)}function us(Ft,rn){let zn=W.blockers.get(Ft)||bfe;od(zn.state==="unblocked"&&rn.state==="blocked"||zn.state==="blocked"&&rn.state==="blocked"||zn.state==="blocked"&&rn.state==="proceeding"||zn.state==="blocked"&&rn.state==="unblocked"||zn.state==="proceeding"&&rn.state==="unblocked",`Invalid blocker state transition: ${zn.state} -> ${rn.state}`);let Oi=new Map(W.blockers);Oi.set(Ft,rn),He({blockers:Oi})}function _r({currentLocation:Ft,nextLocation:rn,historyAction:zn}){if(xe.size===0)return;xe.size>1&&pv(!1,"A router only supports one blocker at a time");let Oi=Array.from(xe.entries()),[Ki,gs]=Oi[Oi.length-1],ur=W.blockers.get(Ki);if(!(ur&&ur.state==="proceeding")&&gs({currentLocation:Ft,nextLocation:rn,historyAction:zn}))return Ki}function uo(Ft){let rn=ZD(404,{pathname:Ft}),zn=d||c,{matches:Oi,route:Ki}=GBe(zn);return{notFoundMatches:Oi,route:Ki,error:rn}}function xs(Ft,rn,zn){if(_=Ft,T=rn,x=zn||null,!I&&W.navigation===KEt){I=!0;let Oi=Ri(W.location,W.matches);Oi!=null&&He({restoreScrollPosition:Oi})}return()=>{_=null,T=null,x=null}}function Fs(Ft,rn){return x&&x(Ft,rn.map(Oi=>RSr(Oi,W.loaderData)))||Ft.key}function eo(Ft,rn){if(_&&T){let zn=Fs(Ft,rn);_[zn]=T()}}function Ri(Ft,rn){if(_){let zn=Fs(Ft,rn),Oi=_[zn];if(typeof Oi=="number")return Oi}return null}function Ls(Ft,rn,zn){if(n.patchRoutesOnNavigation)if(Ft){if(Object.keys(Ft[0].params).length>0)return{active:!0,matches:mke(rn,zn,h,!0)}}else return{active:!0,matches:mke(rn,zn,h,!0)||[]};return{active:!1,matches:null}}async function Cr(Ft,rn,zn,Oi){if(!n.patchRoutesOnNavigation)return{type:"success",matches:Ft};let Ki=Ft;for(;;){let gs=d==null,ur=d||c,vs=l;try{await n.patchRoutesOnNavigation({signal:zn,path:rn,matches:Ki,fetcherKey:Oi,patch:(Do,zs)=>{zn.aborted||V9n(Do,zs,ur,vs,o,!1)}})}catch(Do){return{type:"error",error:Do,partialMatches:Ki}}finally{gs&&!zn.aborted&&(c=[...c])}if(zn.aborted)return{type:"aborted"};let Ir=_G(ur,rn,h),oo=null;if(Ir){if(Object.keys(Ir[0].params).length===0)return{type:"success",matches:Ir};if(oo=mke(ur,rn,h,!0),!(oo&&Ki.lengthzn.route.id===rn[Oi].route.id)}function os(Ft){l={},d=vLe(Ft,o,void 0,l)}function Ks(Ft,rn,zn=!1){let Oi=d==null;V9n(Ft,rn,d||c,l,o,zn),Oi&&(c=[...c],He({}))}return j={get basename(){return h},get future(){return m},get state(){return W},get routes(){return c},get window(){return e},initialize:tt,subscribe:Me,enableScrollRestoration:xs,navigate:rt,fetch:nt,revalidate:Be,createHref:Ft=>n.history.createHref(Ft),encodeLocation:Ft=>n.history.encodeLocation(Ft),getFetcher:hn,resetFetcher:Zi,deleteFetcher:Dr,dispose:Ue,getBlocker:gr,deleteBlocker:ss,patchRoutes:Ks,_internalFetchControllers:we,_internalSetRoutes:os,_internalSetStateDoNotUseOrYouWillBreakYourApp(Ft){He(Ft)}},n.unstable_instrumentations&&(j=XSr(j,n.unstable_instrumentations.map(Ft=>Ft.router).filter(Boolean))),j}function cxr(n){return n!=null&&("formData"in n&&n.formData!=null||"body"in n&&n.body!==void 0)}function uNt(n,e,t,i,r,o){let l,c;if(r){l=[];for(let h of e)if(l.push(h),h.route.id===r){c=h;break}}else l=e,c=e[e.length-1];let d=S5e(i||".",Jet(l),oT(n.pathname,t)||n.pathname,o==="path");if(i==null&&(d.search=n.search,d.hash=n.hash),(i==null||i===""||i===".")&&c){let h=zUt(d.search);if(c.route.index&&!h)d.search=d.search?d.search.replace(/^\?/,"?index&"):"?index";else if(!c.route.index&&h){let p=new URLSearchParams(d.search),m=p.getAll("index");p.delete("index"),m.filter(w=>w).forEach(w=>p.append("index",w));let b=p.toString();d.search=b?`?${b}`:""}}return t!=="/"&&(d.pathname=USr({basename:t,pathname:d.pathname})),z9(d)}function B9n(n,e,t){if(!t||!cxr(t))return{path:e};if(t.formMethod&&!Lxr(t.formMethod))return{path:e,error:ZD(405,{method:t.formMethod})};let i=()=>({path:e,error:ZD(400,{type:"invalid-body"})}),o=(t.formMethod||"get").toUpperCase(),l=Ghi(e);if(t.body!==void 0){if(t.formEncType==="text/plain"){if(!fS(o))return i();let m=typeof t.body=="string"?t.body:t.body instanceof FormData||t.body instanceof URLSearchParams?Array.from(t.body.entries()).reduce((b,[w,_])=>`${b}${w}=${_} -`,""):String(t.body);return{path:e,submission:{formMethod:o,formAction:l,formEncType:t.formEncType,formData:void 0,json:void 0,text:m}}}else if(t.formEncType==="application/json"){if(!fS(o))return i();try{let m=typeof t.body=="string"?JSON.parse(t.body):t.body;return{path:e,submission:{formMethod:o,formAction:l,formEncType:t.formEncType,formData:void 0,json:m,text:void 0}}}catch{return i()}}}od(typeof FormData=="function","FormData is not available in this environment");let c,d;if(t.formData)c=hNt(t.formData),d=t.formData;else if(t.body instanceof FormData)c=hNt(t.body),d=t.body;else if(t.body instanceof URLSearchParams)c=t.body,d=G9n(c);else if(t.body==null)c=new URLSearchParams,d=new FormData;else try{c=new URLSearchParams(t.body),d=G9n(c)}catch{return i()}let h={formMethod:o,formAction:l,formEncType:t&&t.formEncType||"application/x-www-form-urlencoded",formData:d,json:void 0,text:void 0};if(fS(h.formMethod))return{path:e,submission:h};let p=QW(e);return n&&p.search&&zUt(p.search)&&c.append("index",""),p.search=`?${c}`,{path:z9(p),submission:h}}function W9n(n,e,t,i,r,o,l,c,d,h,p,m,b,w,_,x,T,I,L,A,M){let O=A?w6(A[1])?A[1].error:A[1].data:void 0,F=r.createURL(o.location),j=r.createURL(d),W;if(p&&o.errors){let ie=Object.keys(o.errors)[0];W=l.findIndex(se=>se.route.id===ie)}else if(A&&w6(A[1])){let ie=A[0];W=l.findIndex(se=>se.route.id===ie)-1}let q=A?A[1].statusCode:void 0,Z=q&&q>=400,ee={currentUrl:F,currentParams:o.matches[0]?.params||{},nextUrl:j,nextParams:l[0].params,...c,actionResult:O,actionStatus:q},G=E5e(l),te=l.map((ie,se)=>{let{route:de}=ie,ne=null;if(W!=null&&se>W)ne=!1;else if(de.lazy)ne=!0;else if(!VUt(de))ne=!1;else if(p){let{shouldLoad:ye}=Whi(de,o.loaderData,o.errors);ne=ye}else uxr(o.loaderData,o.matches[se],ie)&&(ne=!0);if(ne!==null)return dNt(t,i,n,G,ie,h,e,ne);let we=!1;typeof M=="boolean"?we=M:Z?we=!1:(m||F.pathname+F.search===j.pathname+j.search||F.search!==j.search||dxr(o.matches[se],ie))&&(we=!0);let ue={...ee,defaultShouldRevalidate:we},ce=h4e(ie,ue);return dNt(t,i,n,G,ie,h,e,ce,ue,M)}),Q=[];return _.forEach((ie,se)=>{if(p||!l.some(pe=>pe.route.id===ie.routeId)||w.has(se))return;let de=o.fetchers.get(se),ne=de&&de.state!=="idle"&&de.data===void 0,we=_G(T,ie.path,I);if(!we){if(L&&ne)return;Q.push({key:se,routeId:ie.routeId,path:ie.path,matches:null,match:null,request:null,controller:null});return}if(x.has(se))return;let ue=xUe(we,ie.path),ce=new AbortController,ye=vfe(r,ie.path,ce.signal),he=null;if(b.has(se))b.delete(se),he=ege(t,i,ye,we,ue,h,e);else if(ne)m&&(he=ege(t,i,ye,we,ue,h,e));else{let pe;typeof M=="boolean"?pe=M:Z?pe=!1:pe=m;let me={...ee,defaultShouldRevalidate:pe};h4e(ue,me)&&(he=ege(t,i,ye,we,ue,h,e,me))}he&&Q.push({key:se,routeId:ie.routeId,path:ie.path,matches:he,match:ue,request:ye,controller:ce})}),{dsMatches:te,revalidatingFetchers:Q}}function VUt(n){return n.loader!=null||n.middleware!=null&&n.middleware.length>0}function Whi(n,e,t){if(n.lazy)return{shouldLoad:!0,renderFallback:!0};if(!VUt(n))return{shouldLoad:!1,renderFallback:!1};let i=e!=null&&n.id in e,r=t!=null&&t[n.id]!==void 0;if(!i&&r)return{shouldLoad:!1,renderFallback:!1};if(typeof n.loader=="function"&&n.loader.hydrate===!0)return{shouldLoad:!0,renderFallback:!i};let o=!i&&!r;return{shouldLoad:o,renderFallback:o}}function uxr(n,e,t){let i=!e||t.route.id!==e.route.id,r=!n.hasOwnProperty(t.route.id);return i||r}function dxr(n,e){let t=n.route.path;return n.pathname!==e.pathname||t!=null&&t.endsWith("*")&&n.params["*"]!==e.params["*"]}function h4e(n,e){if(n.route.shouldRevalidate){let t=n.route.shouldRevalidate(e);if(typeof t=="boolean")return t}return e.defaultShouldRevalidate}function V9n(n,e,t,i,r,o){let l;if(n){let h=i[n];od(h,`No route found to patch children into: routeId = ${n}`),h.children||(h.children=[]),l=h.children}else l=t;let c=[],d=[];if(e.forEach(h=>{let p=l.find(m=>Vhi(h,m));p?d.push({existingRoute:p,newRoute:h}):c.push(h)}),c.length>0){let h=vLe(c,r,[n||"_","patch",String(l?.length||"0")],i);l.push(...h)}if(o&&d.length>0)for(let h=0;he.children?.some(r=>Vhi(t,r)))??!1:!1}var $9n=new WeakMap,$hi=({key:n,route:e,manifest:t,mapRouteProperties:i})=>{let r=t[e.id];if(od(r,"No route found in manifest"),!r.lazy||typeof r.lazy!="object")return;let o=r.lazy[n];if(!o)return;let l=$9n.get(r);l||(l={},$9n.set(r,l));let c=l[n];if(c)return c;let d=(async()=>{let h=LSr(n),m=r[n]!==void 0&&n!=="hasErrorBoundary";if(h)pv(!h,"Route property "+n+" is not a supported lazy route property. This property will be ignored."),l[n]=Promise.resolve();else if(m)pv(!1,`Route "${r.id}" has a static property "${n}" defined. The lazy property will be ignored.`);else{let b=await o();b!=null&&(Object.assign(r,{[n]:b}),Object.assign(r,i(r)))}typeof r.lazy=="object"&&(r.lazy[n]=void 0,Object.values(r.lazy).every(b=>b===void 0)&&(r.lazy=void 0))})();return l[n]=d,d},z9n=new WeakMap;function hxr(n,e,t,i,r){let o=t[n.id];if(od(o,"No route found in manifest"),!n.lazy)return{lazyRoutePromise:void 0,lazyHandlerPromise:void 0};if(typeof n.lazy=="function"){let p=z9n.get(o);if(p)return{lazyRoutePromise:p,lazyHandlerPromise:p};let m=(async()=>{od(typeof n.lazy=="function","No lazy route function found");let b=await n.lazy(),w={};for(let _ in b){let x=b[_];if(x===void 0)continue;let T=ISr(_),L=o[_]!==void 0&&_!=="hasErrorBoundary";T?pv(!T,"Route property "+_+" is not a supported property to be returned from a lazy route function. This property will be ignored."):L?pv(!L,`Route "${o.id}" has a static property "${_}" defined but its lazy function is also returning a value for this property. The lazy route property "${_}" will be ignored.`):w[_]=x}Object.assign(o,w),Object.assign(o,{...i(o),lazy:void 0})})();return z9n.set(o,m),m.catch(()=>{}),{lazyRoutePromise:m,lazyHandlerPromise:m}}let l=Object.keys(n.lazy),c=[],d;for(let p of l){if(r&&r.includes(p))continue;let m=$hi({key:p,route:n,manifest:t,mapRouteProperties:i});m&&(c.push(m),p===e&&(d=m))}let h=c.length>0?Promise.all(c).then(()=>{}):void 0;return h?.catch(()=>{}),d?.catch(()=>{}),{lazyRoutePromise:h,lazyHandlerPromise:d}}async function U9n(n){let e=n.matches.filter(r=>r.shouldLoad),t={};return(await Promise.all(e.map(r=>r.resolve()))).forEach((r,o)=>{t[e[o].route.id]=r}),t}async function fxr(n){return n.matches.some(e=>e.route.middleware)?zhi(n,()=>U9n(n)):U9n(n)}function zhi(n,e){return pxr(n,e,i=>{if(Txr(i))throw i;return i},Sxr,t);function t(i,r,o){if(o)return Promise.resolve(Object.assign(o.value,{[r]:{type:"error",result:i}}));{let{matches:l}=n,c=Math.min(Math.max(l.findIndex(h=>h.route.id===r),0),Math.max(l.findIndex(h=>h.shouldCallHandler()),0)),d=CG(l,l[c].route.id).route.id;return Promise.resolve({[d]:{type:"error",result:i}})}}}async function pxr(n,e,t,i,r){let{matches:o,request:l,params:c,context:d,unstable_pattern:h}=n,p=o.flatMap(b=>b.route.middleware?b.route.middleware.map(w=>[b.route.id,w]):[]);return await Uhi({request:l,params:c,context:d,unstable_pattern:h},p,e,t,i,r)}async function Uhi(n,e,t,i,r,o,l=0){let{request:c}=n;if(c.signal.aborted)throw c.signal.reason??new Error(`Request aborted: ${c.method} ${c.url}`);let d=e[l];if(!d)return await t();let[h,p]=d,m,b=async()=>{if(m)throw new Error("You may only call `next()` once per middleware");try{return m={value:await Uhi(n,e,t,i,r,o,l+1)},m.value}catch(w){return m={value:await o(w,h,m)},m.value}};try{let w=await p(n,b),_=w!=null?i(w):void 0;return r(_)?_:m?_??m.value:(m={value:await b()},m.value)}catch(w){return await o(w,h,m)}}function qhi(n,e,t,i,r){let o=$hi({key:"middleware",route:i.route,manifest:e,mapRouteProperties:n}),l=hxr(i.route,fS(t.method)?"action":"loader",e,n,r);return{middleware:o,route:l.lazyRoutePromise,handler:l.lazyHandlerPromise}}function dNt(n,e,t,i,r,o,l,c,d=null,h){let p=!1,m=qhi(n,e,t,r,o);return{...r,_lazyPromises:m,shouldLoad:c,shouldRevalidateArgs:d,shouldCallHandler(b){return p=!0,d?typeof h=="boolean"?h4e(r,{...d,defaultShouldRevalidate:h}):typeof b=="boolean"?h4e(r,{...d,defaultShouldRevalidate:b}):h4e(r,d):c},resolve(b){let{lazy:w,loader:_,middleware:x}=r.route,T=p||c||b&&!fS(t.method)&&(w||_),I=x&&x.length>0&&!_&&!w;return T&&(fS(t.method)||!I)?mxr({request:t,unstable_pattern:i,match:r,lazyHandlerPromise:m?.handler,lazyRoutePromise:m?.route,handlerOverride:b,scopedContext:l}):Promise.resolve({type:"data",result:void 0})}}}function ege(n,e,t,i,r,o,l,c=null){return i.map(d=>d.route.id!==r.route.id?{...d,shouldLoad:!1,shouldRevalidateArgs:c,shouldCallHandler:()=>!1,_lazyPromises:qhi(n,e,t,d,o),resolve:()=>Promise.resolve({type:"data",result:void 0})}:dNt(n,e,t,E5e(i),d,o,l,!0,c))}async function gxr(n,e,t,i,r,o){t.some(h=>h._lazyPromises?.middleware)&&await Promise.all(t.map(h=>h._lazyPromises?.middleware));let l={request:e,unstable_pattern:E5e(t),params:t[0].params,context:r,matches:t},d=await n({...l,fetcherKey:i,runClientMiddleware:h=>{let p=l;return zhi(p,()=>h({...p,fetcherKey:i,runClientMiddleware:()=>{throw new Error("Cannot call `runClientMiddleware()` from within an `runClientMiddleware` handler")}}))}});try{await Promise.all(t.flatMap(h=>[h._lazyPromises?.handler,h._lazyPromises?.route]))}catch{}return d}async function mxr({request:n,unstable_pattern:e,match:t,lazyHandlerPromise:i,lazyRoutePromise:r,handlerOverride:o,scopedContext:l}){let c,d,h=fS(n.method),p=h?"action":"loader",m=b=>{let w,_=new Promise((I,L)=>w=L);d=()=>w(),n.signal.addEventListener("abort",d);let x=I=>typeof b!="function"?Promise.reject(new Error(`You cannot call the handler for a route which defines a boolean "${p}" [routeId: ${t.route.id}]`)):b({request:n,unstable_pattern:e,params:t.params,context:l},...I!==void 0?[I]:[]),T=(async()=>{try{return{type:"data",result:await(o?o(L=>x(L)):x())}}catch(I){return{type:"error",result:I}}})();return Promise.race([T,_])};try{let b=h?t.route.action:t.route.loader;if(i||r)if(b){let w,[_]=await Promise.all([m(b).catch(x=>{w=x}),i,r]);if(w!==void 0)throw w;c=_}else{await i;let w=h?t.route.action:t.route.loader;if(w)[c]=await Promise.all([m(w),r]);else if(p==="action"){let _=new URL(n.url),x=_.pathname+_.search;throw ZD(405,{method:n.method,pathname:x,routeId:t.route.id})}else return{type:"data",result:void 0}}else if(b)c=await m(b);else{let w=new URL(n.url),_=w.pathname+w.search;throw ZD(404,{pathname:_})}}catch(b){return{type:"error",result:b}}finally{d&&n.signal.removeEventListener("abort",d)}return c}async function bxr(n){let e=n.headers.get("Content-Type");return e&&/\bapplication\/json\b/.test(e)?n.body==null?null:n.json():n.text()}async function vxr(n){let{result:e,type:t}=n;if($Ut(e)){let i;try{i=await bxr(e)}catch(r){return{type:"error",error:r}}return t==="error"?{type:"error",error:new x5e(e.status,e.statusText,i),statusCode:e.status,headers:e.headers}:{type:"data",data:i,statusCode:e.status,headers:e.headers}}return t==="error"?X9n(e)?e.data instanceof Error?{type:"error",error:e.data,statusCode:e.init?.status,headers:e.init?.headers?new Headers(e.init.headers):void 0}:{type:"error",error:Cxr(e),statusCode:wLe(e)?e.status:void 0,headers:e.init?.headers?new Headers(e.init.headers):void 0}:{type:"error",error:e,statusCode:wLe(e)?e.status:void 0}:X9n(e)?{type:"data",data:e.data,statusCode:e.init?.status,headers:e.init?.headers?new Headers(e.init.headers):void 0}:{type:"data",data:e}}function wxr(n,e,t,i,r){let o=n.headers.get("Location");if(od(o,"Redirects returned/thrown from loaders/actions must have a Location header"),!WUt(o)){let l=i.slice(0,i.findIndex(c=>c.route.id===t)+1);o=uNt(new URL(e.url),l,r,o),n.headers.set("Location",o)}return n}function q9n(n,e,t,i){let r=["about:","blob:","chrome:","chrome-untrusted:","content:","data:","devtools:","file:","filesystem:","javascript:"];if(WUt(n)){let o=n,l=o.startsWith("//")?new URL(e.protocol+o):new URL(o);if(r.includes(l.protocol))throw new Error("Invalid redirect location");let c=oT(l.pathname,t)!=null;if(l.origin===e.origin&&c)return l.pathname+l.search+l.hash}try{let o=i.createURL(n);if(r.includes(o.protocol))throw new Error("Invalid redirect location")}catch{}return n}function vfe(n,e,t,i){let r=n.createURL(Ghi(e)).toString(),o={signal:t};if(i&&fS(i.formMethod)){let{formMethod:l,formEncType:c}=i;o.method=l.toUpperCase(),c==="application/json"?(o.headers=new Headers({"Content-Type":c}),o.body=JSON.stringify(i.json)):c==="text/plain"?o.body=i.text:c==="application/x-www-form-urlencoded"&&i.formData?o.body=hNt(i.formData):o.body=i.formData}return new Request(r,o)}function hNt(n){let e=new URLSearchParams;for(let[t,i]of n.entries())e.append(t,typeof i=="string"?i:i.name);return e}function G9n(n){let e=new FormData;for(let[t,i]of n.entries())e.append(t,i);return e}function yxr(n,e,t,i=!1,r=!1){let o={},l=null,c,d=!1,h={},p=t&&w6(t[1])?t[1].error:void 0;return n.forEach(m=>{if(!(m.route.id in e))return;let b=m.route.id,w=e[b];if(od(!qne(w),"Cannot handle redirect results in processLoaderData"),w6(w)){let _=w.error;if(p!==void 0&&(_=p,p=void 0),l=l||{},r)l[b]=_;else{let x=CG(n,b);l[x.route.id]==null&&(l[x.route.id]=_)}i||(o[b]=Bhi),d||(d=!0,c=wLe(w.error)?w.error.status:500),w.headers&&(h[b]=w.headers)}else o[b]=w.data,w.statusCode&&w.statusCode!==200&&!d&&(c=w.statusCode),w.headers&&(h[b]=w.headers)}),p!==void 0&&t&&(l={[t[0]]:p},t[2]&&(o[t[2]]=void 0)),{loaderData:o,errors:l,statusCode:c||200,loaderHeaders:h}}function K9n(n,e,t,i,r,o){let{loaderData:l,errors:c}=yxr(e,t,i);return r.filter(d=>!d.matches||d.matches.some(h=>h.shouldLoad)).forEach(d=>{let{key:h,match:p,controller:m}=d;if(m&&m.signal.aborted)return;let b=o[h];if(od(b,"Did not find corresponding fetcher result"),w6(b)){let w=CG(n.matches,p?.route.id);c&&c[w.route.id]||(c={...c,[w.route.id]:b.error}),n.fetchers.delete(h)}else if(qne(b))od(!1,"Unhandled fetcher revalidation redirect");else{let w=YH(b.data);n.fetchers.set(h,w)}}),{loaderData:l,errors:c}}function Y9n(n,e,t,i){let r=Object.entries(e).filter(([,o])=>o!==Bhi).reduce((o,[l,c])=>(o[l]=c,o),{});for(let o of t){let l=o.route.id;if(!e.hasOwnProperty(l)&&n.hasOwnProperty(l)&&o.route.loader&&(r[l]=n[l]),i&&i.hasOwnProperty(l))break}return r}function Z9n(n){return n?w6(n[1])?{actionData:{}}:{actionData:{[n[0]]:n[1].data}}:{}}function CG(n,e){return(e?n.slice(0,n.findIndex(i=>i.route.id===e)+1):[...n]).reverse().find(i=>i.route.hasErrorBoundary===!0)||n[0]}function GBe(n){let e=n.length===1?n[0]:n.find(t=>t.index||!t.path||t.path==="/")||{id:"__shim-error-route__"};return{matches:[{params:{},pathname:"",pathnameBase:"",route:e}],route:e}}function ZD(n,{pathname:e,routeId:t,method:i,type:r,message:o}={}){let l="Unknown Server Error",c="Unknown @remix-run/router error";return n===400?(l="Bad Request",i&&e&&t?c=`You made a ${i} request to "${e}" but did not provide a \`loader\` for route "${t}", so there is no way to handle the request.`:r==="invalid-body"&&(c="Unable to encode submission body")):n===403?(l="Forbidden",c=`Route "${t}" does not match URL "${e}"`):n===404?(l="Not Found",c=`No route matches URL "${e}"`):n===405&&(l="Method Not Allowed",i&&e&&t?c=`You made a ${i.toUpperCase()} request to "${e}" but did not provide an \`action\` for route "${t}", so there is no way to handle the request.`:i&&(c=`Invalid request method "${i.toUpperCase()}"`)),new x5e(n||500,l,new Error(c),!0)}function KBe(n){let e=Object.entries(n);for(let t=e.length-1;t>=0;t--){let[i,r]=e[t];if(qne(r))return{key:i,result:r}}}function Ghi(n){let e=typeof n=="string"?QW(n):n;return z9({...e,hash:""})}function _xr(n,e){return n.pathname!==e.pathname||n.search!==e.search?!1:n.hash===""?e.hash!=="":n.hash===e.hash?!0:e.hash!==""}function Cxr(n){return new x5e(n.init?.status??500,n.init?.statusText??"Internal Server Error",n.data)}function Sxr(n){return n!=null&&typeof n=="object"&&Object.entries(n).every(([e,t])=>typeof e=="string"&&xxr(t))}function xxr(n){return n!=null&&typeof n=="object"&&"type"in n&&"result"in n&&(n.type==="data"||n.type==="error")}function Exr(n){return $Ut(n.result)&&jhi.has(n.result.status)}function w6(n){return n.type==="error"}function qne(n){return(n&&n.type)==="redirect"}function X9n(n){return typeof n=="object"&&n!=null&&"type"in n&&"data"in n&&"init"in n&&n.type==="DataWithResponseInit"}function $Ut(n){return n!=null&&typeof n.status=="number"&&typeof n.statusText=="string"&&typeof n.headers=="object"&&typeof n.body<"u"}function kxr(n){return jhi.has(n)}function Txr(n){return $Ut(n)&&kxr(n.status)&&n.headers.has("Location")}function Lxr(n){return rxr.has(n.toUpperCase())}function fS(n){return nxr.has(n.toUpperCase())}function zUt(n){return new URLSearchParams(n).getAll("index").some(e=>e==="")}function xUe(n,e){let t=typeof e=="string"?QW(e).search:e.search;if(n[n.length-1].route.index&&zUt(t||""))return n[n.length-1];let i=Mhi(n);return i[i.length-1]}function Q9n(n){let{formMethod:e,formAction:t,formEncType:i,text:r,formData:o,json:l}=n;if(!(!e||!t||!i)){if(r!=null)return{formMethod:e,formAction:t,formEncType:i,formData:void 0,json:void 0,text:r};if(o!=null)return{formMethod:e,formAction:t,formEncType:i,formData:o,json:void 0,text:void 0};if(l!==void 0)return{formMethod:e,formAction:t,formEncType:i,formData:void 0,json:l,text:void 0}}}function YEt(n,e){return e?{state:"loading",location:n,formMethod:e.formMethod,formAction:e.formAction,formEncType:e.formEncType,formData:e.formData,json:e.json,text:e.text}:{state:"loading",location:n,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0}}function Dxr(n,e){return{state:"submitting",location:n,formMethod:e.formMethod,formAction:e.formAction,formEncType:e.formEncType,formData:e.formData,json:e.json,text:e.text}}function Vxe(n,e){return n?{state:"loading",formMethod:n.formMethod,formAction:n.formAction,formEncType:n.formEncType,formData:n.formData,json:n.json,text:n.text,data:e}:{state:"loading",formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0,data:e}}function Ixr(n,e){return{state:"submitting",formMethod:n.formMethod,formAction:n.formAction,formEncType:n.formEncType,formData:n.formData,json:n.json,text:n.text,data:e?e.data:void 0}}function YH(n){return{state:"idle",formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0,data:n}}function Axr(n,e){try{let t=n.sessionStorage.getItem(Hhi);if(t){let i=JSON.parse(t);for(let[r,o]of Object.entries(i||{}))o&&Array.isArray(o)&&e.set(r,new Set(o||[]))}}catch{}}function Rxr(n,e){if(e.size>0){let t={};for(let[i,r]of e)t[i]=[...r];try{n.sessionStorage.setItem(Hhi,JSON.stringify(t))}catch(i){pv(!1,`Failed to save applied view transitions in sessionStorage (${i}).`)}}}function J9n(){let n,e,t=new Promise((i,r)=>{n=async o=>{i(o);try{await t}catch{}},e=async o=>{r(o);try{await t}catch{}}});return{promise:t,resolve:n,reject:e}}var uoe=D.createContext(null);uoe.displayName="DataRouter";var k5e=D.createContext(null);k5e.displayName="DataRouterState";var Khi=D.createContext(!1);function Mxr(){return D.useContext(Khi)}var UUt=D.createContext({isTransitioning:!1});UUt.displayName="ViewTransition";var Yhi=D.createContext(new Map);Yhi.displayName="Fetchers";var Oxr=D.createContext(null);Oxr.displayName="Await";var d8=D.createContext(null);d8.displayName="Navigation";var ett=D.createContext(null);ett.displayName="Location";var U3=D.createContext({outlet:null,matches:[],isDataRoute:!1});U3.displayName="Route";var qUt=D.createContext(null);qUt.displayName="RouteError";var Zhi="REACT_ROUTER_ERROR",Nxr="REDIRECT",Pxr="ROUTE_ERROR_RESPONSE";function Fxr(n){if(n.startsWith(`${Zhi}:${Nxr}:{`))try{let e=JSON.parse(n.slice(28));if(typeof e=="object"&&e&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.location=="string"&&typeof e.reloadDocument=="boolean"&&typeof e.replace=="boolean")return e}catch{}}function jxr(n){if(n.startsWith(`${Zhi}:${Pxr}:{`))try{let e=JSON.parse(n.slice(40));if(typeof e=="object"&&e&&typeof e.status=="number"&&typeof e.statusText=="string")return new x5e(e.status,e.statusText,e.data)}catch{}}function Hxr(n,{relative:e}={}){od(J1e(),"useHref() may be used only in the context of a component.");let{basename:t,navigator:i}=D.useContext(d8),{hash:r,pathname:o,search:l}=T5e(n,{relative:e}),c=o;return t!=="/"&&(c=o==="/"?t:iM([t,o])),i.createHref({pathname:c,search:l,hash:r})}function J1e(){return D.useContext(ett)!=null}function B1(){return od(J1e(),"useLocation() may be used only in the context of a component."),D.useContext(ett).location}var Xhi="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Qhi(n){D.useContext(d8).static||D.useLayoutEffect(n)}function $E(){let{isDataRoute:n}=D.useContext(U3);return n?eEr():Bxr()}function Bxr(){od(J1e(),"useNavigate() may be used only in the context of a component.");let n=D.useContext(uoe),{basename:e,navigator:t}=D.useContext(d8),{matches:i}=D.useContext(U3),{pathname:r}=B1(),o=JSON.stringify(Jet(i)),l=D.useRef(!1);return Qhi(()=>{l.current=!0}),D.useCallback((d,h={})=>{if(pv(l.current,Xhi),!l.current)return;if(typeof d=="number"){t.go(d);return}let p=S5e(d,JSON.parse(o),r,h.relative==="path");n==null&&e!=="/"&&(p.pathname=p.pathname==="/"?e:iM([e,p.pathname])),(h.replace?t.replace:t.push)(p,h.state,h)},[e,t,o,r,n])}var Wxr=D.createContext(null);function Vxr(n){let e=D.useContext(U3).outlet;return D.useMemo(()=>e&&D.createElement(Wxr.Provider,{value:n},e),[e,n])}function ttt(){let{matches:n}=D.useContext(U3),e=n[n.length-1];return e?e.params:{}}function T5e(n,{relative:e}={}){let{matches:t}=D.useContext(U3),{pathname:i}=B1(),r=JSON.stringify(Jet(t));return D.useMemo(()=>S5e(n,JSON.parse(r),i,e==="path"),[n,r,i,e])}function $xr(n,e,t){od(J1e(),"useRoutes() may be used only in the context of a component.");let{navigator:i}=D.useContext(d8),{matches:r}=D.useContext(U3),o=r[r.length-1],l=o?o.params:{},c=o?o.pathname:"/",d=o?o.pathnameBase:"/",h=o&&o.route;{let T=h&&h.path||"";nfi(c,!h||T.endsWith("*")||T.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${c}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. - -Please change the parent to .`)}let p=B1(),m;m=p;let b=m.pathname||"/",w=b;if(d!=="/"){let T=d.replace(/^\//,"").split("/");w="/"+b.replace(/^\//,"").split("/").slice(T.length).join("/")}let _=_G(n,{pathname:w});return pv(h||_!=null,`No routes matched location "${m.pathname}${m.search}${m.hash}" `),pv(_==null||_[_.length-1].route.element!==void 0||_[_.length-1].route.Component!==void 0||_[_.length-1].route.lazy!==void 0,`Matched leaf route at location "${m.pathname}${m.search}${m.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`),Kxr(_&&_.map(T=>Object.assign({},T,{params:Object.assign({},l,T.params),pathname:iM([d,i.encodeLocation?i.encodeLocation(T.pathname.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:T.pathname]),pathnameBase:T.pathnameBase==="/"?d:iM([d,i.encodeLocation?i.encodeLocation(T.pathnameBase.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:T.pathnameBase])})),r,t)}function zxr(){let n=Xxr(),e=wLe(n)?`${n.status} ${n.statusText}`:n instanceof Error?n.message:JSON.stringify(n),t=n instanceof Error?n.stack:null,i="rgba(200,200,200, 0.5)",r={padding:"0.5rem",backgroundColor:i},o={padding:"2px 4px",backgroundColor:i},l=null;return console.error("Error handled by React Router default ErrorBoundary:",n),l=D.createElement(D.Fragment,null,D.createElement("p",null,"💿 Hey developer 👋"),D.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",D.createElement("code",{style:o},"ErrorBoundary")," or"," ",D.createElement("code",{style:o},"errorElement")," prop on your route.")),D.createElement(D.Fragment,null,D.createElement("h2",null,"Unexpected Application Error!"),D.createElement("h3",{style:{fontStyle:"italic"}},e),t?D.createElement("pre",{style:r},t):null,l)}var Uxr=D.createElement(zxr,null),Jhi=class extends D.Component{constructor(n){super(n),this.state={location:n.location,revalidation:n.revalidation,error:n.error}}static getDerivedStateFromError(n){return{error:n}}static getDerivedStateFromProps(n,e){return e.location!==n.location||e.revalidation!=="idle"&&n.revalidation==="idle"?{error:n.error,location:n.location,revalidation:n.revalidation}:{error:n.error!==void 0?n.error:e.error,location:e.location,revalidation:n.revalidation||e.revalidation}}componentDidCatch(n,e){this.props.onError?this.props.onError(n,e):console.error("React Router caught the following error during render",n)}render(){let n=this.state.error;if(this.context&&typeof n=="object"&&n&&"digest"in n&&typeof n.digest=="string"){const t=jxr(n.digest);t&&(n=t)}let e=n!==void 0?D.createElement(U3.Provider,{value:this.props.routeContext},D.createElement(qUt.Provider,{value:n,children:this.props.component})):this.props.children;return this.context?D.createElement(qxr,{error:n},e):e}};Jhi.contextType=Khi;var ZEt=new WeakMap;function qxr({children:n,error:e}){let{basename:t}=D.useContext(d8);if(typeof e=="object"&&e&&"digest"in e&&typeof e.digest=="string"){let i=Fxr(e.digest);if(i){let r=ZEt.get(e);if(r)throw r;let o=Nhi(i.location,t);if(Ohi&&!ZEt.get(e))if(o.isExternal||i.reloadDocument)window.location.href=o.absoluteURL||o.to;else{const l=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(o.to,{replace:i.replace}));throw ZEt.set(e,l),l}return D.createElement("meta",{httpEquiv:"refresh",content:`0;url=${o.absoluteURL||o.to}`})}}return n}function Gxr({routeContext:n,match:e,children:t}){let i=D.useContext(uoe);return i&&i.static&&i.staticContext&&(e.route.errorElement||e.route.ErrorBoundary)&&(i.staticContext._deepestRenderedBoundaryId=e.route.id),D.createElement(U3.Provider,{value:n},t)}function Kxr(n,e=[],t){let i=t?.state;if(n==null){if(!i)return null;if(i.errors)n=i.matches;else if(e.length===0&&!i.initialized&&i.matches.length>0)n=i.matches;else return null}let r=n,o=i?.errors;if(o!=null){let p=r.findIndex(m=>m.route.id&&o?.[m.route.id]!==void 0);od(p>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(o).join(",")}`),r=r.slice(0,Math.min(r.length,p+1))}let l=!1,c=-1;if(t&&i){l=i.renderFallback;for(let p=0;p=0?r=r.slice(0,c+1):r=[r[0]];break}}}}let d=t?.onError,h=i&&d?(p,m)=>{d(p,{location:i.location,params:i.matches?.[0]?.params??{},unstable_pattern:E5e(i.matches),errorInfo:m})}:void 0;return r.reduceRight((p,m,b)=>{let w,_=!1,x=null,T=null;i&&(w=o&&m.route.id?o[m.route.id]:void 0,x=m.route.errorElement||Uxr,l&&(c<0&&b===0?(nfi("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),_=!0,T=null):c===b&&(_=!0,T=m.route.hydrateFallbackElement||null)));let I=e.concat(r.slice(0,b+1)),L=()=>{let A;return w?A=x:_?A=T:m.route.Component?A=D.createElement(m.route.Component,null):m.route.element?A=m.route.element:A=p,D.createElement(Gxr,{match:m,routeContext:{outlet:p,matches:I,isDataRoute:i!=null},children:A})};return i&&(m.route.ErrorBoundary||m.route.errorElement||b===0)?D.createElement(Jhi,{location:i.location,revalidation:i.revalidation,component:x,error:w,children:L(),routeContext:{outlet:null,matches:I,isDataRoute:!0},onError:h}):L()},null)}function GUt(n){return`${n} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function efi(n){let e=D.useContext(uoe);return od(e,GUt(n)),e}function tfi(n){let e=D.useContext(k5e);return od(e,GUt(n)),e}function Yxr(n){let e=D.useContext(U3);return od(e,GUt(n)),e}function KUt(n){let e=Yxr(n),t=e.matches[e.matches.length-1];return od(t.route.id,`${n} can only be used on routes that contain a unique "id"`),t.route.id}function Zxr(){return KUt("useRouteId")}function Xxr(){let n=D.useContext(qUt),e=tfi("useRouteError"),t=KUt("useRouteError");return n!==void 0?n:e.errors?.[t]}var Qxr=0;function Jxr(n){let{router:e,basename:t}=efi("useBlocker"),i=tfi("useBlocker"),[r,o]=D.useState(""),l=D.useCallback(c=>{if(typeof n!="function")return!!n;if(t==="/")return n(c);let{currentLocation:d,nextLocation:h,historyAction:p}=c;return n({currentLocation:{...d,pathname:oT(d.pathname,t)||d.pathname},nextLocation:{...h,pathname:oT(h.pathname,t)||h.pathname},historyAction:p})},[t,n]);return D.useEffect(()=>{let c=String(++Qxr);return o(c),()=>e.deleteBlocker(c)},[e]),D.useEffect(()=>{r!==""&&e.getBlocker(r,l)},[e,r,l]),r&&i.blockers.has(r)?i.blockers.get(r):bfe}function eEr(){let{router:n}=efi("useNavigate"),e=KUt("useNavigate"),t=D.useRef(!1);return Qhi(()=>{t.current=!0}),D.useCallback(async(r,o={})=>{pv(t.current,Xhi),t.current&&(typeof r=="number"?await n.navigate(r):await n.navigate(r,{fromRouteId:e,...o}))},[n,e])}var e7n={};function nfi(n,e,t){!e&&!e7n[n]&&(e7n[n]=!0,pv(!1,t))}var t7n={};function n7n(n,e){!n&&!t7n[e]&&(t7n[e]=!0,console.warn(e))}var tEr="useOptimistic",i7n=V9[tEr],nEr=()=>{};function iEr(n){return i7n?i7n(n):[n,nEr]}function rEr(n){let e={hasErrorBoundary:n.hasErrorBoundary||n.ErrorBoundary!=null||n.errorElement!=null};return n.Component&&(n.element&&pv(!1,"You should not include both `Component` and `element` on your route - `Component` will be used."),Object.assign(e,{element:D.createElement(n.Component),Component:void 0})),n.HydrateFallback&&(n.hydrateFallbackElement&&pv(!1,"You should not include both `HydrateFallback` and `hydrateFallbackElement` on your route - `HydrateFallback` will be used."),Object.assign(e,{hydrateFallbackElement:D.createElement(n.HydrateFallback),HydrateFallback:void 0})),n.ErrorBoundary&&(n.errorElement&&pv(!1,"You should not include both `ErrorBoundary` and `errorElement` on your route - `ErrorBoundary` will be used."),Object.assign(e,{errorElement:D.createElement(n.ErrorBoundary),ErrorBoundary:void 0})),e}var sEr=["HydrateFallback","hydrateFallbackElement"],oEr=class{constructor(){this.status="pending",this.promise=new Promise((e,t)=>{this.resolve=i=>{this.status==="pending"&&(this.status="resolved",e(i))},this.reject=i=>{this.status==="pending"&&(this.status="rejected",t(i))}})}};function aEr({router:n,flushSync:e,onError:t,unstable_useTransitions:i}){i=Mxr()||i;let[o,l]=D.useState(n.state),[c,d]=iEr(o),[h,p]=D.useState(),[m,b]=D.useState({isTransitioning:!1}),[w,_]=D.useState(),[x,T]=D.useState(),[I,L]=D.useState(),A=D.useRef(new Map),M=D.useCallback((W,{deletedFetchers:q,newErrors:Z,flushSync:ee,viewTransitionOpts:G})=>{Z&&t&&Object.values(Z).forEach(Q=>t(Q,{location:W.location,params:W.matches[0]?.params??{},unstable_pattern:E5e(W.matches)})),W.fetchers.forEach((Q,ie)=>{Q.data!==void 0&&A.current.set(ie,Q.data)}),q.forEach(Q=>A.current.delete(Q)),n7n(ee===!1||e!=null,'You provided the `flushSync` option to a router update, but you are not using the `` from `react-router/dom` so `ReactDOM.flushSync()` is unavailable. Please update your app to `import { RouterProvider } from "react-router/dom"` and ensure you have `react-dom` installed as a dependency to use the `flushSync` option.');let te=n.window!=null&&n.window.document!=null&&typeof n.window.document.startViewTransition=="function";if(n7n(G==null||te,"You provided the `viewTransition` option to a router update, but you do not appear to be running in a DOM environment as `window.startViewTransition` is not available."),!G||!te){e&&ee?e(()=>l(W)):i===!1?l(W):D.startTransition(()=>{i===!0&&d(Q=>r7n(Q,W)),l(W)});return}if(e&&ee){e(()=>{x&&(w?.resolve(),x.skipTransition()),b({isTransitioning:!0,flushSync:!0,currentLocation:G.currentLocation,nextLocation:G.nextLocation})});let Q=n.window.document.startViewTransition(()=>{e(()=>l(W))});Q.finished.finally(()=>{e(()=>{_(void 0),T(void 0),p(void 0),b({isTransitioning:!1})})}),e(()=>T(Q));return}x?(w?.resolve(),x.skipTransition(),L({state:W,currentLocation:G.currentLocation,nextLocation:G.nextLocation})):(p(W),b({isTransitioning:!0,flushSync:!1,currentLocation:G.currentLocation,nextLocation:G.nextLocation}))},[n.window,e,x,w,i,d,t]);D.useLayoutEffect(()=>n.subscribe(M),[n,M]),D.useEffect(()=>{m.isTransitioning&&!m.flushSync&&_(new oEr)},[m]),D.useEffect(()=>{if(w&&h&&n.window){let W=h,q=w.promise,Z=n.window.document.startViewTransition(async()=>{i===!1?l(W):D.startTransition(()=>{i===!0&&d(ee=>r7n(ee,W)),l(W)}),await q});Z.finished.finally(()=>{_(void 0),T(void 0),p(void 0),b({isTransitioning:!1})}),T(Z)}},[h,w,n.window,i,d]),D.useEffect(()=>{w&&h&&c.location.key===h.location.key&&w.resolve()},[w,x,c.location,h]),D.useEffect(()=>{!m.isTransitioning&&I&&(p(I.state),b({isTransitioning:!0,flushSync:!1,currentLocation:I.currentLocation,nextLocation:I.nextLocation}),L(void 0))},[m.isTransitioning,I]);let O=D.useMemo(()=>({createHref:n.createHref,encodeLocation:n.encodeLocation,go:W=>n.navigate(W),push:(W,q,Z)=>n.navigate(W,{state:q,preventScrollReset:Z?.preventScrollReset}),replace:(W,q,Z)=>n.navigate(W,{replace:!0,state:q,preventScrollReset:Z?.preventScrollReset})}),[n]),F=n.basename||"/",j=D.useMemo(()=>({router:n,navigator:O,static:!1,basename:F,onError:t}),[n,O,F,t]);return D.createElement(D.Fragment,null,D.createElement(uoe.Provider,{value:j},D.createElement(k5e.Provider,{value:c},D.createElement(Yhi.Provider,{value:A.current},D.createElement(UUt.Provider,{value:m},D.createElement(uEr,{basename:F,location:c.location,navigationType:c.historyAction,navigator:O,unstable_useTransitions:i},D.createElement(lEr,{routes:n.routes,future:n.future,state:c,isStatic:!1,onError:t})))))),null)}function r7n(n,e){return{...n,navigation:e.navigation.state!=="idle"?e.navigation:n.navigation,revalidation:e.revalidation!=="idle"?e.revalidation:n.revalidation,actionData:e.navigation.state!=="submitting"?e.actionData:n.actionData,fetchers:e.fetchers}}var lEr=D.memo(cEr);function cEr({routes:n,future:e,state:t,isStatic:i,onError:r}){return $xr(n,void 0,{state:t,isStatic:i,onError:r})}function wme({to:n,replace:e,state:t,relative:i}){od(J1e()," may be used only in the context of a component.");let{static:r}=D.useContext(d8);pv(!r," must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.");let{matches:o}=D.useContext(U3),{pathname:l}=B1(),c=$E(),d=S5e(n,Jet(o),l,i==="path"),h=JSON.stringify(d);return D.useEffect(()=>{c(JSON.parse(h),{replace:e,state:t,relative:i})},[c,h,i,e,t]),null}function ifi(n){return Vxr(n.context)}function uEr({basename:n="/",children:e=null,location:t,navigationType:i="POP",navigator:r,static:o=!1,unstable_useTransitions:l}){od(!J1e(),"You cannot render a inside another . You should never have more than one in your app.");let c=n.replace(/^\/*/,"/"),d=D.useMemo(()=>({basename:c,navigator:r,static:o,unstable_useTransitions:l,future:{}}),[c,r,o,l]);typeof t=="string"&&(t=QW(t));let{pathname:h="/",search:p="",hash:m="",state:b=null,key:w="default",unstable_mask:_}=t,x=D.useMemo(()=>{let T=oT(h,c);return T==null?null:{location:{pathname:T,search:p,hash:m,state:b,key:w,unstable_mask:_},navigationType:i}},[c,h,p,m,b,w,i,_]);return pv(x!=null,` is not able to match the URL "${h}${p}${m}" because it does not start with the basename, so the won't render anything.`),x==null?null:D.createElement(d8.Provider,{value:d},D.createElement(ett.Provider,{children:e,value:x}))}var EUe="get",kUe="application/x-www-form-urlencoded";function ntt(n){return typeof HTMLElement<"u"&&n instanceof HTMLElement}function dEr(n){return ntt(n)&&n.tagName.toLowerCase()==="button"}function hEr(n){return ntt(n)&&n.tagName.toLowerCase()==="form"}function fEr(n){return ntt(n)&&n.tagName.toLowerCase()==="input"}function pEr(n){return!!(n.metaKey||n.altKey||n.ctrlKey||n.shiftKey)}function gEr(n,e){return n.button===0&&(!e||e==="_self")&&!pEr(n)}var YBe=null;function mEr(){if(YBe===null)try{new FormData(document.createElement("form"),0),YBe=!1}catch{YBe=!0}return YBe}var bEr=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function XEt(n){return n!=null&&!bEr.has(n)?(pv(!1,`"${n}" is not a valid \`encType\` for \`

    \`/\`\` and will default to "${kUe}"`),null):n}function vEr(n,e){let t,i,r,o,l;if(hEr(n)){let c=n.getAttribute("action");i=c?oT(c,e):null,t=n.getAttribute("method")||EUe,r=XEt(n.getAttribute("enctype"))||kUe,o=new FormData(n)}else if(dEr(n)||fEr(n)&&(n.type==="submit"||n.type==="image")){let c=n.form;if(c==null)throw new Error('Cannot submit a